Repository: gradio-app/gradio
Pull request: Fix `assert_configs_are_equivalent_besides_ids()`
Fixes the `assert_configs_are_equivalent_besides_ids()` to check for other fields in the `dependencies`. Adds a test as well to catch the mistake @freddyaboulton pointed out.

Fixes: #1653 

Problem statement:
assert_configs_are_equivalent_besides_ids does not check documentation field
### Describe the bug

`assert_configs_are_equivalent_besides_ids` does not check all fields in the config, e.g. `documentation`, `api_name`

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Reproduction

Repro. I would expect the assertion to fail in this case since `documentation` is different.

```python
import copy
from gradio.utils import assert_configs_are_equivalent_besides_ids

config1 = {'version': '3.0.20\n', 'mode': 'blocks', 'dev_mode': True,
           'components': [{'id': 1, 'type': 'textbox',
            'props': {'lines': 1, 'max_lines': 20, 'placeholder': 'What is your name?', 'value': '',
                      'show_label': True, 'name': 'textbox', 'visible': True, 'style': {}}},
                      {'id': 2, 'type': 'textbox', 'props': {'lines': 1, 'max_lines': 20, 'value': '', 'show_label': True, 'name': 'textbox', 'visible': True, 'style': {}}}, {'id': 3, 'type': 'image', 'props': {'image_mode': 'RGB', 'source': 'upload', 'tool': 'editor', 'streaming': False, 'show_label': True, 'name': 'image', 'visible': True, 'style': {'height': 54, 'width': 240}}}], 'theme': 'default', 'css': None, 'enable_queue': False, 'layout': {'id': 0, 'children': [{'id': 1}, {'id': 2}, {'id': 3}]},
           'dependencies': [
               {'targets': [1], 'trigger': 'submit', 'inputs': [1], 'outputs': [2],
                'backend_fn': True, 'js': None, 'status_tracker': None, 'queue': None, 'api_name': 'greet',
                 'scroll_to_output': False, 'show_progress': True, 'documentation': [['(str): text'], ['(str | None): text']]
               }
           ]}

config2 = copy.deepcopy(config1)
config2['dependencies'][0]['documentation'] = None

assert assert_configs_are_equivalent_besides_ids(config1, config2)
```

### Screenshot

_No response_

### Logs

```shell
N/a
```


### System Info

```shell
Latest main
```


### Severity

annoying

Hints from issue discussion:

Commit message: formatting

Patch to review:
diff --git a/gradio/test_data/blocks_configs.py b/gradio/test_data/blocks_configs.py
index 2ecfb8afaa..b8a4eb5fb8 100644
--- a/gradio/test_data/blocks_configs.py
+++ b/gradio/test_data/blocks_configs.py
@@ -162,6 +162,9 @@
             "js": None,
             "status_tracker": None,
             "queue": None,
+            "api_name": None,
+            "scroll_to_output": False,
+            "show_progress": True,
         },
         {
             "targets": [13],
@@ -172,6 +175,9 @@
             "js": None,
             "status_tracker": None,
             "queue": None,
+            "api_name": None,
+            "scroll_to_output": False,
+            "show_progress": True,
         },
         {
             "targets": [],
@@ -182,6 +188,9 @@
             "js": False,
             "status_tracker": None,
             "queue": None,
+            "api_name": None,
+            "scroll_to_output": False,
+            "show_progress": True,
         },
     ],
 }
@@ -358,6 +367,9 @@
             "js": None,
             "status_tracker": None,
             "queue": None,
+            "api_name": None,
+            "scroll_to_output": False,
+            "show_progress": True,
         },
         {
             "targets": [13],
@@ -368,6 +380,9 @@
             "js": None,
             "status_tracker": None,
             "queue": None,
+            "api_name": None,
+            "scroll_to_output": False,
+            "show_progress": True,
         },
         {
             "targets": [],
@@ -378,6 +393,9 @@
             "js": False,
             "status_tracker": None,
             "queue": None,
+            "api_name": None,
+            "scroll_to_output": False,
+            "show_progress": True,
         },
     ],
 }
@@ -549,6 +567,9 @@
             "inputs": [2, 6],
             "outputs": [7],
             "status_tracker": None,
+            "api_name": None,
+            "scroll_to_output": False,
+            "show_progress": True,
         },
         {
             "targets": [13],
@@ -556,6 +577,9 @@
             "inputs": [2, 11],
             "outputs": [12],
             "status_tracker": None,
+            "api_name": None,
+            "scroll_to_output": False,
+            "show_progress": True,
         },
     ],
 }
diff --git a/gradio/utils.py b/gradio/utils.py
index 50477d16fe..f16b14852b 100644
--- a/gradio/utils.py
+++ b/gradio/utils.py
@@ -3,23 +3,23 @@
 from __future__ import annotations
 
 import asyncio
+import copy
 import inspect
 import json
 import json.decoder
 import os
 import random
-import sys
 import warnings
 from copy import deepcopy
 from distutils.version import StrictVersion
 from enum import Enum
+from importlib.metadata import version
 from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, NewType, Type
 
 import aiohttp
 import analytics
 import fsspec.asyn
 import httpx
-import pkg_resources
 import requests
 from pydantic import BaseModel, Json, parse_obj_as
 
@@ -38,7 +38,7 @@
 
 def version_check():
     try:
-        current_pkg_version = pkg_resources.require("gradio")[0].version
+        current_pkg_version = version("gradio")
         latest_pkg_version = requests.get(url=PKG_VERSION_URL).json()["version"]
         if StrictVersion(latest_pkg_version) > StrictVersion(current_pkg_version):
             print(
@@ -49,10 +49,6 @@ def version_check():
                 )
             )
             print("--------")
-    except pkg_resources.DistributionNotFound:
-        warnings.warn(
-            "gradio is not setup or installed properly. Unable to get version info."
-        )
     except json.decoder.JSONDecodeError:
         warnings.warn("unable to parse version details from package URL.")
     except KeyError:
@@ -197,6 +193,9 @@ def get_default_args(func: Callable) -> Dict[str, Any]:
 
 def assert_configs_are_equivalent_besides_ids(config1, config2):
     """Allows you to test if two different Blocks configs produce the same demo."""
+    config1 = copy.deepcopy(config1)
+    config2 = copy.deepcopy(config2)
+
     assert config1["mode"] == config2["mode"], "Modes are different"
     assert config1["theme"] == config2["theme"], "Themes are different"
     assert len(config1["components"]) == len(
@@ -225,13 +224,16 @@ def same_children_recursive(children1, chidren2, mapping):
     same_children_recursive(children1, children2, mapping)
 
     for d1, d2 in zip(config1["dependencies"], config2["dependencies"]):
-        for t1, t2 in zip(d1["targets"], d2["targets"]):
+        for t1, t2 in zip(d1.pop("targets"), d2.pop("targets")):
             assert mapping[t1] == t2, "{} does not match {}".format(d1, d2)
-        assert d1["trigger"] == d2["trigger"], "{} does not match {}".format(d1, d2)
-        for i1, i2 in zip(d1["inputs"], d2["inputs"]):
+        for i1, i2 in zip(d1.pop("inputs"), d2.pop("inputs")):
             assert mapping[i1] == i2, "{} does not match {}".format(d1, d2)
-        for o1, o2 in zip(d1["outputs"], d2["outputs"]):
+        for o1, o2 in zip(d1.pop("outputs"), d2.pop("outputs")):
             assert mapping[o1] == o2, "{} does not match {}".format(d1, d2)
+        # status tracker is ignored
+        d1.pop("status_tracker", None)
+        d2.pop("status_tracker", None)
+        assert d1 == d2, "{} does not match {}".format(d1, d2)
 
     return True
 
diff --git a/test/test_blocks.py b/test/test_blocks.py
index f1feb9c4f5..a1a09de264 100644
--- a/test/test_blocks.py
+++ b/test/test_blocks.py
@@ -1,4 +1,5 @@
 import asyncio
+import copy
 import random
 import time
 import unittest
@@ -86,7 +87,11 @@ def fake_func():
 
         config = demo.get_config_file()
         config.pop("version")  # remove version key
-        self.assertTrue(assert_configs_are_equivalent_besides_ids(XRAY_CONFIG, config))
+        self.assertTrue(
+            assert_configs_are_equivalent_besides_ids(
+                copy.deepcopy(XRAY_CONFIG), config
+            )
+        )
 
     def test_load_from_config(self):
         def update(name):
diff --git a/test/test_utils.py b/test/test_utils.py
index c9de3a21ca..6c3d19ef17 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -1,12 +1,10 @@
-import asyncio
-import ipaddress
+import copy
 import os
 import unittest
 import unittest.mock as mock
 import warnings
 from typing import Literal
 
-import pkg_resources
 import pytest
 import pytest_asyncio
 import requests
@@ -37,18 +35,6 @@
 
 
 class TestUtils(unittest.TestCase):
-    @mock.patch("pkg_resources.require")
-    def test_should_fail_with_distribution_not_found(self, mock_require):
-        mock_require.side_effect = pkg_resources.DistributionNotFound()
-
-        with warnings.catch_warnings(record=True) as w:
-            warnings.simplefilter("always")
-            version_check()
-            self.assertEqual(
-                str(w[-1].message),
-                "gradio is not setup or installed properly. Unable to get version info.",
-            )
-
     @mock.patch("requests.get")
     def test_should_warn_with_unable_to_parse(self, mock_get):
         mock_get.side_effect = json.decoder.JSONDecodeError("Expecting value", "", 0)
@@ -130,20 +116,99 @@ def test_get_ip_without_internet(self, mock_get):
 class TestAssertConfigsEquivalent(unittest.TestCase):
     def test_same_configs(self):
         self.assertTrue(
-            assert_configs_are_equivalent_besides_ids(XRAY_CONFIG, XRAY_CONFIG)
+            assert_configs_are_equivalent_besides_ids(
+                copy.deepcopy(XRAY_CONFIG), copy.deepcopy(XRAY_CONFIG)
+            )
         )
 
     def test_equivalent_configs(self):
         self.assertTrue(
-            assert_configs_are_equivalent_besides_ids(XRAY_CONFIG, XRAY_CONFIG_DIFF_IDS)
+            assert_configs_are_equivalent_besides_ids(
+                copy.deepcopy(XRAY_CONFIG), copy.deepcopy(XRAY_CONFIG_DIFF_IDS)
+            )
         )
 
     def test_different_configs(self):
         with self.assertRaises(AssertionError):
             assert_configs_are_equivalent_besides_ids(
-                XRAY_CONFIG_WITH_MISTAKE, XRAY_CONFIG
+                copy.deepcopy(XRAY_CONFIG_WITH_MISTAKE), copy.deepcopy(XRAY_CONFIG)
             )
 
+    def test_different_dependencies(self):
+        config1 = {
+            "version": "3.0.20\n",
+            "mode": "blocks",
+            "dev_mode": True,
+            "components": [
+                {
+                    "id": 1,
+                    "type": "textbox",
+                    "props": {
+                        "lines": 1,
+                        "max_lines": 20,
+                        "placeholder": "What is your name?",
+                        "value": "",
+                        "show_label": True,
+                        "name": "textbox",
+                        "visible": True,
+                        "style": {},
+                    },
+                },
+                {
+                    "id": 2,
+                    "type": "textbox",
+                    "props": {
+                        "lines": 1,
+                        "max_lines": 20,
+                        "value": "",
+                        "show_label": True,
+                        "name": "textbox",
+                        "visible": True,
+                        "style": {},
+                    },
+                },
+                {
+                    "id": 3,
+                    "type": "image",
+                    "props": {
+                        "image_mode": "RGB",
+                        "source": "upload",
+                        "tool": "editor",
+                        "streaming": False,
+                        "show_label": True,
+                        "name": "image",
+                        "visible": True,
+                        "style": {"height": 54, "width": 240},
+                    },
+                },
+            ],
+            "theme": "default",
+            "css": None,
+            "enable_queue": False,
+            "layout": {"id": 0, "children": [{"id": 1}, {"id": 2}, {"id": 3}]},
+            "dependencies": [
+                {
+                    "targets": [1],
+                    "trigger": "submit",
+                    "inputs": [1],
+                    "outputs": [2],
+                    "backend_fn": True,
+                    "js": None,
+                    "status_tracker": None,
+                    "queue": None,
+                    "api_name": "greet",
+                    "scroll_to_output": False,
+                    "show_progress": True,
+                    "documentation": [["(str): text"], ["(str | None): text"]],
+                }
+            ],
+        }
+
+        config2 = copy.deepcopy(config1)
+        config2["dependencies"][0]["documentation"] = None
+        with self.assertRaises(AssertionError):
+            assert_configs_are_equivalent_besides_ids(config1, config2)
+
 
 class TestFormatNERList(unittest.TestCase):
     def test_format_ner_list_standard(self):
@@ -200,7 +265,7 @@ async def client():
 class TestRequest:
     @pytest.mark.asyncio
     async def test_get(self):
-        client_response = await Request(
+        client_response: Request = await Request(
             method=Request.Method.GET,
             url="http://headers.jsontest.com/",
         )
@@ -210,7 +275,7 @@ async def test_get(self):
 
     @pytest.mark.asyncio
     async def test_post(self):
-        client_response = await Request(
+        client_response: Request = await Request(
             method=Request.Method.POST,
             url="https://reqres.in/api/users",
             json={"name": "morpheus", "job": "leader"},
@@ -228,7 +293,7 @@ class TestModel(BaseModel):
             id: str
             createdAt: str
 
-        client_response = await Request(
+        client_response: Request = await Request(
             method=Request.Method.POST,
             url="https://reqres.in/api/users",
             json={"name": "morpheus", "job": "leader"},
@@ -242,7 +307,7 @@ class TestModel(BaseModel):
             name: Literal[str] = "John"
             job: str
 
-        client_response = await Request(
+        client_response: Request = await Request(
             method=Request.Method.POST,
             url="https://reqres.in/api/users",
             json={"name": "morpheus", "job": "leader"},
@@ -261,7 +326,7 @@ class ResponseValidationException(Exception):
 
         validate_response_data.side_effect = Exception()
 
-        client_response = await Request(
+        client_response: Request = await Request(
             method=Request.Method.GET,
             url="https://reqres.in/api/users",
             exception_type=ResponseValidationException,
@@ -275,7 +340,7 @@ def has_name(response):
                 return response
             raise Exception
 
-        client_response = await Request(
+        client_response: Request = await Request(
             method=Request.Method.POST,
             url="https://reqres.in/api/users",
             json={"name": "morpheus", "job": "leader"},
@@ -294,7 +359,7 @@ def has_name(response):
                     return response
             raise Exception
 
-        client_response = await Request(
+        client_response: Request = await Request(
             method=Request.Method.POST,
             url="https://reqres.in/api/users",
             json={"name": "morpheus", "job": "leader"},


Code review comments:
diff_hunk: @@ -225,13 +224,16 @@ def same_children_recursive(children1, chidren2, mapping):
     same_children_recursive(children1, children2, mapping)
 
     for d1, d2 in zip(config1["dependencies"], config2["dependencies"]):
-        for t1, t2 in zip(d1["targets"], d2["targets"]):
+        for t1, t2 in zip(d1.pop("targets"), d2.pop("targets")):
             assert mapping[t1] == t2, "{} does not match {}".format(d1, d2)
path: gradio/utils.py
text: @user1:
Shall we use f"" strings over here and below?

@author:
Sg will update

diff_hunk: @@ -130,20 +116,99 @@ def test_get_ip_without_internet(self, mock_get):
 class TestAssertConfigsEquivalent(unittest.TestCase):
     def test_same_configs(self):
         self.assertTrue(
-            assert_configs_are_equivalent_besides_ids(XRAY_CONFIG, XRAY_CONFIG)
+            assert_configs_are_equivalent_besides_ids(
+                copy.deepcopy(XRAY_CONFIG), copy.deepcopy(XRAY_CONFIG)
path: test/test_utils.py
text: @user1:
I don't think this copy is necessary since the util performs a copy. 

@author:
Yeah I'll remove these copy.deepcopies

diff_hunk: @@ -86,7 +87,11 @@ def fake_func():
 
         config = demo.get_config_file()
         config.pop("version")  # remove version key
path: test/test_blocks.py
text: @user1:
hmm, instead of popping `version` in individual tests, wouldn't it be better to do it in the `assert_configs_are_equivalent_besides_ids`?

@user2:
Yea I think it will be better if the logic for ignoring fields is in `assert_configs_are_equivalent..` as opposed to the calling code. Maybe we can add a `ignore_fields: List[str]` parameter to the function that gives uses the ability to specify what they want to ignore. I can see use cases where you would want to ignore the version key and others where you wouldn't. 

I don't think this is necessary to merge this PR though. If anything I can file an issue to implement in the future.

@author:
Sounds good! Removed the line and added a parameter `root_keys` which specifies which keys to test for equivlance