Repository: gradio-app/gradio
Pull request: Raise error if Blocks has duplicate component with same IDs
Fixes: #2320 

Also avoids making a call to figure out the IP address if analytics is disabled. The reason for that change is that I was working with airplane Wifi and noticed it was *extremely* slow to launch a Gradio Interface because it was taking a long time to get the IP address. The real solution is to use async requests, but this is a temporary bandaid

Problem statement:
Components/Blocks have duplicate IDs if rendered more than once
### Describe the bug

If a component or Interface is rendered, but has already been rendered previously, it will add blocks with duplicate IDs to the config. Here's an example:

```py
io3b = gr.Interface(
    lambda x: [x["mask"], x["image"]],
    gr.ImageMask(),
    [gr.Image(), gr.Image()],
)

with gr.Blocks():
  io3b.render()
  io3b.render()
```

We assume uniqueness of component IDs so this can cause all sorts of problems. The safest solution would be to raise an `Exception` to prevent a component/blocks from being rendered multiple times.

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Reproduction

See above

### Screenshot

_No response_

### Logs

```shell
N/A
```


### System Info

```shell
3.3.1
```


### Severity

serious, but I can work around it

Hints from issue discussion:

Commit message: version

Patch to review:
diff --git a/gradio/blocks.py b/gradio/blocks.py
index 5e57fe8c2c..d4f0a8f2be 100644
--- a/gradio/blocks.py
+++ b/gradio/blocks.py
@@ -34,6 +34,7 @@
     document_component_api,
     set_documentation_group,
 )
+from gradio.exceptions import DuplicateBlockError
 from gradio.utils import component_or_layout_class, delete_none
 
 set_documentation_group("blocks")
@@ -62,6 +63,10 @@ def render(self):
         """
         Adds self into appropriate BlockContext
         """
+        if Context.root_block is not None and self._id in Context.root_block.blocks:
+            raise DuplicateBlockError(
+                f"A block with id: {self._id} has already been rendered in the current Blocks."
+            )
         if Context.block is not None:
             Context.block.children.append(self)
         if Context.root_block is not None:
@@ -72,8 +77,7 @@ def render(self):
     def unrender(self):
         """
         Removes self from BlockContext if it has been rendered (otherwise does nothing).
-        Only deletes the first occurrence of self in BlockContext. Removes from the
-        layout and collection of Blocks, but does not delete any event triggers.
+        Removes self from the layout and collection of blocks, but does not delete any event triggers.
         """
         if Context.block is not None:
             try:
@@ -434,7 +438,7 @@ def __init__(
         self.width = None
         self.height = None
 
-        self.ip_address = utils.get_local_ip_address()
+        self.ip_address = None
         self.is_space = True if os.getenv("SYSTEM") == "spaces" else False
         self.favicon_path = None
         self.auth = None
@@ -444,16 +448,17 @@ def __init__(
         self.title = title
         self.show_api = True
 
-        data = {
-            "mode": self.mode,
-            "ip_address": self.ip_address,
-            "custom_css": self.css is not None,
-            "theme": self.theme,
-            "version": pkgutil.get_data(__name__, "version.txt")
-            .decode("ascii")
-            .strip(),
-        }
         if self.analytics_enabled:
+            self.ip_address = utils.get_local_ip_address()
+            data = {
+                "mode": self.mode,
+                "ip_address": self.ip_address,
+                "custom_css": self.css is not None,
+                "theme": self.theme,
+                "version": pkgutil.get_data(__name__, "version.txt")
+                .decode("ascii")
+                .strip(),
+            }
             utils.initiated_analytics(data)
 
     @property
@@ -606,6 +611,15 @@ def __repr__(self):
 
     def render(self):
         if Context.root_block is not None:
+            if self._id in Context.root_block.blocks:
+                raise DuplicateBlockError(
+                    f"A block with id: {self._id} has already been rendered in the current Blocks."
+                )
+            if not set(Context.root_block.blocks).isdisjoint(self.blocks):
+                raise DuplicateBlockError(
+                    "At least one block in this Blocks has already been rendered."
+                )
+
             Context.root_block.blocks.update(self.blocks)
             Context.root_block.fns.extend(self.fns)
             for dependency in self.dependencies:
@@ -624,6 +638,7 @@ def render(self):
                         dependency["api_name"] = api_name_
                 Context.root_block.dependencies.append(dependency)
             Context.root_block.temp_dirs = Context.root_block.temp_dirs | self.temp_dirs
+
         if Context.block is not None:
             Context.block.children.extend(self.children)
 
@@ -1136,20 +1151,20 @@ def reverse(text):
             except ImportError:
                 pass
 
-        data = {
-            "launch_method": "browser" if inbrowser else "inline",
-            "is_google_colab": is_colab,
-            "is_sharing_on": self.share,
-            "share_url": self.share_url,
-            "ip_address": self.ip_address,
-            "enable_queue": self.enable_queue,
-            "show_tips": self.show_tips,
-            "server_name": server_name,
-            "server_port": server_port,
-            "is_spaces": self.is_space,
-            "mode": self.mode,
-        }
-        if hasattr(self, "analytics_enabled") and self.analytics_enabled:
+        if getattr(self, "analytics_enabled", False):
+            data = {
+                "launch_method": "browser" if inbrowser else "inline",
+                "is_google_colab": is_colab,
+                "is_sharing_on": self.share,
+                "share_url": self.share_url,
+                "ip_address": self.ip_address,
+                "enable_queue": self.enable_queue,
+                "show_tips": self.show_tips,
+                "server_name": server_name,
+                "server_port": server_port,
+                "is_spaces": self.is_space,
+                "mode": self.mode,
+            }
             utils.launch_analytics(data)
 
         utils.show_tip(self)
diff --git a/gradio/exceptions.py b/gradio/exceptions.py
index 980bad12e3..d8594acd98 100644
--- a/gradio/exceptions.py
+++ b/gradio/exceptions.py
@@ -1,3 +1,7 @@
+class DuplicateBlockError(KeyError):
+    pass
+
+
 class Error(Exception):
     def __init__(self, message: str):
         self.message = message
diff --git a/gradio/interface.py b/gradio/interface.py
index 014913f4f4..39406306fe 100644
--- a/gradio/interface.py
+++ b/gradio/interface.py
@@ -383,23 +383,22 @@ def clean_html(raw_html):
 
         self.favicon_path = None
 
-        data = {
-            "mode": self.mode,
-            "fn": fn,
-            "inputs": inputs,
-            "outputs": outputs,
-            "live": live,
-            "ip_address": self.ip_address,
-            "interpretation": interpretation,
-            "allow_flagging": allow_flagging,
-            "custom_css": self.css is not None,
-            "theme": self.theme,
-            "version": pkgutil.get_data(__name__, "version.txt")
-            .decode("ascii")
-            .strip(),
-        }
-
         if self.analytics_enabled:
+            data = {
+                "mode": self.mode,
+                "fn": fn,
+                "inputs": inputs,
+                "outputs": outputs,
+                "live": live,
+                "ip_address": self.ip_address,
+                "interpretation": interpretation,
+                "allow_flagging": allow_flagging,
+                "custom_css": self.css is not None,
+                "theme": self.theme,
+                "version": pkgutil.get_data(__name__, "version.txt")
+                .decode("ascii")
+                .strip(),
+            }
             utils.initiated_analytics(data)
 
         utils.version_check()
diff --git a/gradio/version.txt b/gradio/version.txt
index 2f4b60750d..d1e43136d8 100644
--- a/gradio/version.txt
+++ b/gradio/version.txt
@@ -1 +1 @@
-3.4
+3.4.1b0
diff --git a/test/test_blocks.py b/test/test_blocks.py
index 76c4c0dfe8..be7e75a76b 100644
--- a/test/test_blocks.py
+++ b/test/test_blocks.py
@@ -16,6 +16,7 @@
 import wandb
 
 import gradio as gr
+from gradio.exceptions import DuplicateBlockError
 from gradio.routes import PredictBody
 from gradio.test_data.blocks_configs import XRAY_CONFIG
 from gradio.utils import assert_configs_are_equivalent_besides_ids
@@ -444,5 +445,52 @@ def test_with_generic_update(self):
         }
 
 
+class TestDuplicateBlockError:
+    def test_error(self):
+        with pytest.raises(DuplicateBlockError):
+            t = gr.Textbox()
+            with gr.Blocks():
+                t.render()
+                gr.Number()
+                t.render()
+
+        with pytest.raises(DuplicateBlockError):
+            with gr.Blocks():
+                t = gr.Textbox()
+                t.render()
+
+        with pytest.raises(DuplicateBlockError):
+            io = gr.Interface(lambda x: x, gr.Textbox(), gr.Textbox())
+            with gr.Blocks():
+                io.render()
+                io.render()
+
+        with pytest.raises(DuplicateBlockError):
+            t = gr.Textbox()
+            io = gr.Interface(lambda x: x, t, gr.Textbox())
+            with gr.Blocks():
+                io.render()
+                t.render()
+
+    def test_no_error(self):
+        t = gr.Textbox()
+        t2 = gr.Textbox()
+        with gr.Blocks():
+            t.render()
+            t2.render()
+
+        t = gr.Textbox()
+        io = gr.Interface(lambda x: x, t, gr.Textbox())
+        with gr.Blocks():
+            io.render()
+            gr.Textbox()
+
+        io = gr.Interface(lambda x: x, gr.Textbox(), gr.Textbox())
+        io2 = gr.Interface(lambda x: x, gr.Textbox(), gr.Textbox())
+        with gr.Blocks():
+            io.render()
+            io2.render()
+
+
 if __name__ == "__main__":
     unittest.main()


Code review comments:
diff_hunk: @@ -1,3 +1,7 @@
+class DuplicateBlockError(KeyError):
path: gradio/exceptions.py
text: @user1:
I think KeyError is usually reserved for when a key in a dictionary is not present? Minor point but I think a `ValueError` or `LookupError` would be more appropriate here.

@author:
Sure, will change to `ValueError`