Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Waiting to run
storybook-build / changes (push) Waiting to run
storybook-build / :storybook-build (push) Blocked by required conditions
Sync Gradio Skills to Hugging Face / sync-skills (push) Waiting to run
functional / changes (push) Waiting to run
functional / build-frontend (push) Blocked by required conditions
functional / functional-test-SSR=false (push) Blocked by required conditions
functional / functional-reload (push) Blocked by required conditions
functional / functional-test-SSR=true (push) Blocked by required conditions
hygiene / hygiene-test (push) Waiting to run
python / changes (push) Waiting to run
python / build (push) Blocked by required conditions
python / test-ubuntu-latest-flaky (push) Blocked by required conditions
python / test-ubuntu-latest-not-flaky (push) Blocked by required conditions
python / test-windows-latest-flaky (push) Blocked by required conditions
python / test-windows-latest-not-flaky (push) Blocked by required conditions
js / changes (push) Waiting to run
js / js-test (push) Blocked by required conditions
docs-build / changes (push) Waiting to run
docs-build / docs-build (push) Blocked by required conditions
docs-build / website-build (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

1155 lines
37 KiB
Python

from __future__ import annotations
import json
import os
import sys
import warnings
from collections.abc import Sequence
from pathlib import Path
from typing import Literal
from unittest.mock import patch
import numpy as np
import pytest
from gradio_client.exceptions import AppError
from hypothesis import given, settings # ty: ignore[unresolved-import]
from hypothesis import strategies as st # ty: ignore[unresolved-import]
import gradio as gr
from gradio import EventData, Request
from gradio.exceptions import Error
from gradio.external_utils import format_ner_list
from gradio.utils import (
FileSize,
SyncToAsyncIterator,
UnhashableKeyDict,
_parse_file_size,
abspath,
append_unique_suffix,
assert_configs_are_equivalent_besides_ids,
check_function_inputs_match,
colab_check,
delete_none,
diff,
download_if_url,
error_payload,
get_extension_from_file_path_or_url,
get_function_description,
get_function_params,
get_heartbeat_rate,
get_icon_path,
get_type_hints,
ipython_check,
is_allowed_file,
is_hosted_notebook,
is_in_or_equal,
is_special_typed_parameter,
parse_escaped_json,
safe_aclose_iterator,
safe_deepcopy,
sanitize_list_for_csv,
sanitize_value_for_csv,
tex2svg,
validate_url,
)
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
class TestUtils:
@patch("IPython.get_ipython")
def test_colab_check_no_ipython(self, mock_get_ipython):
mock_get_ipython.return_value = None
assert colab_check() is False
@patch("IPython.get_ipython")
def test_ipython_check_import_fail(self, mock_get_ipython):
mock_get_ipython.side_effect = ImportError()
assert ipython_check() is False
@patch("IPython.get_ipython")
def test_ipython_check_no_ipython(self, mock_get_ipython):
mock_get_ipython.return_value = None
assert ipython_check() is False
def test_download_if_url_doesnt_crash_on_connection_error(self):
in_article = "placeholder"
out_article = download_if_url(in_article)
assert out_article == in_article
# non-printable characters are not allowed in URL address
in_article = "text\twith\rnon-printable\nASCII\x00characters"
out_article = download_if_url(in_article)
assert out_article == in_article
# only files with HTTP(S) URL can be downloaded
in_article = "ftp://localhost/tmp/index.html"
out_article = download_if_url(in_article)
assert out_article == in_article
in_article = "file:///C:/tmp/index.html"
out_article = download_if_url(in_article)
assert out_article == in_article
# this address will raise ValueError during parsing
in_article = "https://[unmatched_bracket#?:@/index.html"
out_article = download_if_url(in_article)
assert out_article == in_article
@pytest.mark.flaky
def test_download_if_url_correct_parse(self):
in_article = "https://huggingface.co/datasets/gradio/custom-html-gallery/blob/main/manifest.json"
out_article = download_if_url(in_article)
assert out_article != in_article
def test_is_hosted_notebook_false(self):
assert not is_hosted_notebook()
def test_kaggle_check_true_when_run_type_set(self):
with patch.dict(
os.environ, {"KAGGLE_KERNEL_RUN_TYPE": "Interactive"}, clear=True
):
assert is_hosted_notebook()
def test_assert_configs_are_equivalent():
test_dir = Path(__file__).parent / "test_files"
with open(test_dir / "xray_config.json") as fp:
xray_config = json.load(fp)
with open(test_dir / "xray_config_diff_ids.json") as fp:
xray_config_diff_ids = json.load(fp)
with open(test_dir / "xray_config_wrong.json") as fp:
xray_config_wrong = json.load(fp)
assert assert_configs_are_equivalent_besides_ids(xray_config, xray_config)
assert assert_configs_are_equivalent_besides_ids(xray_config, xray_config_diff_ids)
with pytest.raises(ValueError):
assert_configs_are_equivalent_besides_ids(xray_config, xray_config_wrong)
def test_get_heartbeat_rate(monkeypatch):
# Defaults to 15 seconds when nothing is configured.
monkeypatch.delenv("GRADIO_HEARTBEAT_INTERVAL", raising=False)
monkeypatch.delenv("GRADIO_IS_E2E_TEST", raising=False)
assert get_heartbeat_rate() == 15
# GRADIO_HEARTBEAT_INTERVAL overrides the default with a float value.
monkeypatch.setenv("GRADIO_HEARTBEAT_INTERVAL", "1.5")
assert get_heartbeat_rate() == 1.5
# It takes precedence over the GRADIO_IS_E2E_TEST fallback.
monkeypatch.setenv("GRADIO_IS_E2E_TEST", "1")
assert get_heartbeat_rate() == 1.5
# Falls back to the E2E rate when only GRADIO_IS_E2E_TEST is set.
monkeypatch.delenv("GRADIO_HEARTBEAT_INTERVAL", raising=False)
assert get_heartbeat_rate() == 0.25
# An invalid value warns and falls back to the default.
monkeypatch.delenv("GRADIO_IS_E2E_TEST", raising=False)
monkeypatch.setenv("GRADIO_HEARTBEAT_INTERVAL", "not-a-number")
with pytest.warns(UserWarning):
assert get_heartbeat_rate() == 15
# A zero or negative value would busy-loop asyncio.sleep(), so it warns
# and falls back to the default.
for bad_value in ("0", "-5"):
monkeypatch.setenv("GRADIO_HEARTBEAT_INTERVAL", bad_value)
with pytest.warns(UserWarning):
assert get_heartbeat_rate() == 15
class TestFormatNERList:
def test_format_ner_list_standard(self):
string = "Wolfgang lives in Berlin"
groups = [
{"entity_group": "PER", "start": 0, "end": 8},
{"entity_group": "LOC", "start": 18, "end": 24},
]
result = [
("", None),
("Wolfgang", "PER"),
(" lives in ", None),
("Berlin", "LOC"),
("", None),
]
assert format_ner_list(string, groups) == result
def test_format_ner_list_empty(self):
string = "I live in a city"
groups = []
result = [("I live in a city", None)]
assert format_ner_list(string, groups) == result
class TestDeleteNone:
"""Credit: https://stackoverflow.com/questions/33797126/proper-way-to-remove-keys-in-dictionary-with-none-values-in-python"""
def test_delete_none(self):
input = {
"a": 12,
"b": 34,
"c": None,
"k": {
"d": 34,
"t": None,
"m": [{"k": 23, "t": None}, [None, 1, 2, 3], {1, 2, None}],
None: 123,
},
}
truth = {
"a": 12,
"b": 34,
"k": {
"d": 34,
"t": None,
"m": [{"k": 23, "t": None}, [None, 1, 2, 3], {1, 2, None}],
None: 123,
},
}
assert delete_none(input) == truth
class TestSanitizeForCSV:
def test_unsafe_value(self):
assert sanitize_value_for_csv("=OPEN()") == "'=OPEN()"
assert sanitize_value_for_csv("=1+2") == "'=1+2"
assert sanitize_value_for_csv('=1+2";=1+2') == "'=1+2\";=1+2"
def test_safe_value(self):
assert sanitize_value_for_csv(4) == 4
assert sanitize_value_for_csv(-44.44) == -44.44
assert sanitize_value_for_csv("1+1=2") == "1+1=2"
assert sanitize_value_for_csv("1aaa2") == "1aaa2"
def test_list(self):
assert sanitize_list_for_csv([4, "def=", "=gh+ij"]) == [4, "def=", "'=gh+ij"]
assert sanitize_list_for_csv(
[["=abc", "def", "gh,+ij"], ["abc", "=def", "+ghij"]]
) == [["'=abc", "def", "'gh,+ij"], ["abc", "'=def", "'+ghij"]]
assert sanitize_list_for_csv([1, ["ab", "=de"]]) == [1, ["ab", "'=de"]]
def test_parse_escaped_json(self):
assert parse_escaped_json("-0.5678") == -0.5678
# Negative numbers get CSV-escaped with a leading "'" on write (#13591)
assert parse_escaped_json("'-0.5678") == -0.5678
with pytest.raises(json.JSONDecodeError):
parse_escaped_json("'not json")
class TestValidateURL:
@pytest.mark.flaky
def test_valid_urls(self):
assert validate_url("https://www.gradio.app")
assert validate_url("http://gradio.dev")
assert validate_url(
"https://upload.wikimedia.org/wikipedia/commons/b/b0/Bengal_tiger_%28Panthera_tigris_tigris%29_female_3_crop.jpg"
)
assert validate_url(
"https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/bread_small.png"
)
def test_invalid_urls(self):
assert not (validate_url("C:/Users/"))
assert not (validate_url("C:\\Users\\"))
assert not (validate_url("/home/user"))
class TestAppendUniqueSuffix:
def test_no_suffix(self):
name = "test"
list_of_names = ["test_1", "test_2"]
assert append_unique_suffix(name, list_of_names) == name
def test_first_suffix(self):
name = "test"
list_of_names = ["test", "test_-1"]
assert append_unique_suffix(name, list_of_names) == "test_1"
def test_later_suffix(self):
name = "test"
list_of_names = ["test", "test_1", "test_2", "test_3"]
assert append_unique_suffix(name, list_of_names) == "test_4"
class TestAbspath:
def test_abspath_no_symlink(self):
resolved_path = str(abspath("../gradio/gradio/test_data/lion.jpg"))
assert ".." not in resolved_path
@pytest.mark.skipif(
sys.platform.startswith("win"),
reason="Windows doesn't allow creation of sym links without administrative privileges",
)
def test_abspath_symlink_path(self):
os.symlink("gradio/test_data", "gradio/test_link", True)
resolved_path = str(abspath("../gradio/gradio/test_link/lion.jpg"))
os.unlink("gradio/test_link")
assert "test_link" in resolved_path
@pytest.mark.skipif(
sys.platform.startswith("win"),
reason="Windows doesn't allow creation of sym links without administrative privileges",
)
def test_abspath_symlink_dir(self):
os.symlink("gradio/test_data", "gradio/test_link", True)
full_path = os.path.join(os.getcwd(), "gradio/test_link/lion.jpg")
resolved_path = str(abspath(full_path))
os.unlink("gradio/test_link")
assert "test_link" in resolved_path
assert full_path == resolved_path
class TestGetTypeHints:
def test_get_type_hints(self):
class F:
def __call__(self, s: str):
return s
class C:
def f(self, s: str):
return s
def f(s: str):
return s
class GenericObject:
pass
test_objs = [F(), C().f, f]
for x in test_objs:
hints = get_type_hints(x)
assert len(hints) == 1
assert hints["s"] is str
assert len(get_type_hints(GenericObject())) == 0
def test_get_type_hints_with_unresolvable_forward_ref(self):
"""get_type_hints should return {} when annotations can't be resolved at runtime."""
def func(x: str) -> "NonExistentType": # noqa: F821, UP037 # ty: ignore[unresolved-reference]
return x
hints = get_type_hints(func)
assert hints == {}
def test_is_special_typed_parameter(self):
def func(a: list[str], b: Literal["a", "b"], c, d: Request, e: Request | None):
pass
hints = get_type_hints(func)
assert not is_special_typed_parameter("a", hints)
assert not is_special_typed_parameter("b", hints)
assert not is_special_typed_parameter("c", hints)
assert is_special_typed_parameter("d", hints)
assert is_special_typed_parameter("e", 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):
class F:
def __call__(self, s: str, evt: EventData):
return s
class C:
def f(self, s: str, evt: EventData):
return s
def f(s: str, evt: EventData):
return s
test_objs = [F(), C().f, f]
with warnings.catch_warnings():
warnings.simplefilter("error") # Ensure there're no warnings raised here.
for x in test_objs:
check_function_inputs_match(x, [None], False)
def test_tex2svg_preserves_matplotlib_backend():
import matplotlib
matplotlib.use("svg")
tex2svg("1+1=2")
assert matplotlib.get_backend() == "svg"
with pytest.raises(
Exception # specifically a pyparsing.ParseException but not important here
):
tex2svg("$$$1+1=2$$$")
assert matplotlib.get_backend() == "svg"
def test_is_in_or_equal():
assert is_in_or_equal("files/lion.jpg", "files/lion.jpg")
assert is_in_or_equal("files/lion.jpg", "files")
assert is_in_or_equal("files/lion.._M.jpg", "files")
assert not is_in_or_equal("files", "files/lion.jpg")
assert is_in_or_equal("/home/usr/notes.txt", "/home/usr/")
assert not is_in_or_equal("/home/usr/subdirectory", "/home/usr/notes.txt")
assert not is_in_or_equal("/home/usr/../../etc/notes.txt", "/home/usr/")
assert not is_in_or_equal("/safe_dir/subdir/../../unsafe_file.txt", "/safe_dir/")
@pytest.mark.skipif(
sys.platform.startswith("win"),
reason="Windows doesn't support POSIX double-slash notation",
)
def test_is_in_or_equal_posix_specific_paths():
assert is_in_or_equal("//foo/..a", "//foo")
assert is_in_or_equal("//foo/asd/", "/foo")
assert is_in_or_equal("//foo/..²", "/foo")
def create_path_string():
return st.lists(
st.one_of(
st.text(
alphabet="ab@1/(",
min_size=1,
),
st.just(".."),
st.just("."),
),
min_size=1,
max_size=10, # Limit depth to avoid excessively long paths
).map(lambda x: os.path.join(*x).replace("(", ".."))
def create_path_list():
return st.lists(create_path_string(), min_size=0, max_size=5)
def my_check(path_1, path_2):
try:
path_1 = Path(path_1).resolve()
path_2 = Path(path_2).resolve()
_ = path_1.relative_to(path_2)
return True
except ValueError:
return False
@settings(derandomize=os.getenv("CI") is not None)
@given(
path_1=create_path_string(),
path_2=create_path_string(),
)
def test_is_in_or_equal_fuzzer(path_1, path_2):
try:
# Convert to absolute paths
abs_path_1 = abspath(path_1)
abs_path_2 = abspath(path_2)
result = is_in_or_equal(abs_path_1, abs_path_2)
assert result == my_check(abs_path_1, abs_path_2)
except Exception as e:
pytest.fail(f"Exception raised: {e}")
@settings(derandomize=os.getenv("CI") is not None)
@given(
path=create_path_string(),
blocked_paths=create_path_list(),
allowed_paths=create_path_list(),
created_paths=create_path_list(),
)
def test_is_allowed_file_fuzzer(
path: Path,
blocked_paths: Sequence[Path],
allowed_paths: Sequence[Path],
created_paths: Sequence[Path],
):
result, reason = is_allowed_file(path, blocked_paths, allowed_paths, created_paths)
assert isinstance(result, bool)
assert reason in [
"in_blocklist",
"allowed",
"not_created_or_allowed",
"created",
]
if result:
assert reason in ("allowed", "created")
elif reason == "in_blocklist":
assert any(is_in_or_equal(path, blocked_path) for blocked_path in blocked_paths)
elif reason == "not_created_or_allowed":
assert not any(
is_in_or_equal(path, allowed_path) for allowed_path in allowed_paths
)
if reason == "allowed":
assert any(is_in_or_equal(path, allowed_path) for allowed_path in allowed_paths)
elif reason == "created":
assert any(is_in_or_equal(path, created_path) for created_path in created_paths)
@pytest.mark.parametrize(
"path,blocked_paths,allowed_paths",
[
("/a/foo.txt", ["/a"], ["/b"], False),
("/b/foo.txt", ["/a"], ["/b"], True),
("/a/../c/foo.txt", ["/c/"], ["/a/"], False),
("/c/../a/foo.txt", ["/c/"], ["/a/"], True),
("/c/foo.txt", ["/c/"], ["/c/foo.txt"], True),
],
)
def is_allowed_file_corner_cases(path, blocked_paths, allowed_paths, result):
assert is_allowed_file(path, blocked_paths, allowed_paths, []) == result
# Additional test for known edge cases
@pytest.mark.parametrize(
"path_1,path_2,expected",
[
("/AAA/a/../a", "/AAA", True),
("//AA/a", "/tmp", False),
("/AAA/..", "/AAA", False),
("/a/b/c", "/d/e/f", False),
(".", "..", True),
("..", ".", False),
("/a/b/./c", "/a/b", True),
("/a/b/../c", "/a", True),
("/a/b/c", "/a/b/c/../d", False),
("/", "/a", False),
("/a", "/", True),
],
)
def test_is_in_or_equal_edge_cases(path_1, path_2, expected):
assert is_in_or_equal(path_1, path_2) == expected
@pytest.mark.parametrize(
"path_or_url, extension",
[
("https://example.com/avatar/xxxx.mp4?se=2023-11-16T06:51:23Z&sp=r", "mp4"),
("/home/user/documents/example.pdf", "pdf"),
("C:\\Users\\user\\documents\\example.png", "png"),
("C:/Users/user/documents/example", ""),
],
)
def test_get_extension_from_file_path_or_url(path_or_url, extension):
assert get_extension_from_file_path_or_url(path_or_url) == extension
@pytest.mark.parametrize(
"old, new, expected_diff",
[
({"a": 1, "b": 2}, {"a": 1, "b": 2}, []),
({}, {"a": 1, "b": 2}, [["add", ["a"], 1], ["add", ["b"], 2]]),
(["a", "b"], {"a": 1, "b": 2}, [["replace", [], {"a": 1, "b": 2}]]),
("abc", "abcdef", [["append", [], "def"]]),
(
[
{"role": "user", "content": "Hello!", "metadata": {"id": 1}},
{"role": "assistant", "content": "b"},
],
[
{
"role": "assistant",
"content": "Thinking...",
"metadata": {"title": "Thinking..."},
},
{"role": "assistant", "content": "b"},
],
[
["replace", [0, "role"], "assistant"],
["replace", [0, "content"], "Thinking..."],
["delete", [0, "metadata", "id"], None],
["add", [0, "metadata", "title"], "Thinking..."],
],
),
(
[
{"role": "user", "content": "Hello!", "metadata": {"id": 1}},
{"role": "assistant", "content": "b"},
],
[
{"role": "user", "content": "No metadata", "metadata": {}},
{"role": "assistant", "content": "b"},
],
[
["replace", [0, "content"], "No metadata"],
["delete", [0, "metadata", "id"], None],
],
),
(
{"data": [[1, 1], [2, 2], [3, 3]]},
{"data": [[1, 1]]},
[
["delete", ["data", 1], None],
["delete", ["data", 1], None],
],
),
],
)
def test_diff(old, new, expected_diff):
assert diff(old, new) == expected_diff
class TestFunctionParams:
def test_regular_function(self):
def func(a: int, b: int = 10, c: str = "default", d=None):
pass
assert get_function_params(func) == [
("a", False, None, int),
("b", True, 10, int),
("c", True, "default", str),
("d", True, None, None),
]
def test_function_no_params(self):
def func():
pass
assert get_function_params(func) == []
def test_lambda_function(self):
assert get_function_params(lambda x, y: x + y) == [
("x", False, None, None),
("y", False, None, None),
]
def test_function_with_args(self):
def func(a, *args):
pass
assert get_function_params(func) == [("a", False, None, None)]
def test_function_with_kwargs(self):
def func(a, **kwargs):
pass
assert get_function_params(func) == [("a", False, None, None)]
def test_function_with_special_args(self):
def func(a, r: Request, b=10):
pass
assert get_function_params(func) == [
("a", False, None, None),
("b", True, 10, None),
]
def func2(a, r: Request | None = None, b="abc"):
pass
assert get_function_params(func2) == [
("a", False, None, None),
("b", True, "abc", None),
]
def test_class_method_skip_first_param(self):
class MyClass:
def method(self, arg1, arg2=42):
pass
assert get_function_params(MyClass().method) == [
("arg1", False, None, None),
("arg2", True, 42, None),
]
def test_static_method_no_skip(self):
class MyClass:
@staticmethod
def method(arg1, arg2=42):
pass
assert get_function_params(MyClass.method) == [
("arg1", False, None, None),
("arg2", True, 42, None),
]
def test_class_method_with_args(self):
class MyClass:
def method(self, a, *args, b=42):
pass
assert get_function_params(MyClass().method) == [("a", False, None, None)]
def test_lambda_with_args(self):
assert get_function_params(lambda x, *args: x) == [("x", False, None, None)]
def test_lambda_with_kwargs(self):
assert get_function_params(lambda x, **kwargs: x) == [("x", False, None, None)]
def test_parse_file_size():
assert _parse_file_size("1kb") == 1 * FileSize.KB
assert _parse_file_size("1mb") == 1 * FileSize.MB
assert _parse_file_size("505 Mb") == 505 * FileSize.MB
class TestUnhashableKeyDict:
def test_set_get_simple(self):
d = UnhashableKeyDict()
d["a"] = 1
assert d["a"] == 1
def test_set_get_unhashable(self):
d = UnhashableKeyDict()
key = [1, 2, 3]
key2 = [1, 2, 3]
d[key] = "value"
assert d[key] == "value"
assert d[key2] == "value"
def test_set_get_numpy_array(self):
d = UnhashableKeyDict()
key = np.array([1, 2, 3])
key2 = np.array([1, 2, 3])
d[key] = "numpy value"
assert d[key2] == "numpy value"
def test_overwrite(self):
d = UnhashableKeyDict()
d["key"] = "old"
d["key"] = "new"
assert d["key"] == "new"
def test_delete(self):
d = UnhashableKeyDict()
d["key"] = "value"
del d["key"]
assert len(d) == 0
with pytest.raises(KeyError):
d["key"]
def test_delete_nonexistent(self):
d = UnhashableKeyDict()
with pytest.raises(KeyError):
del d["nonexistent"]
def test_len(self):
d = UnhashableKeyDict()
assert len(d) == 0
d["a"] = 1
d["b"] = 2
assert len(d) == 2
def test_contains(self):
d = UnhashableKeyDict()
d["key"] = "value"
assert "key" in d
assert "nonexistent" not in d
def test_get_nonexistent(self):
d = UnhashableKeyDict()
with pytest.raises(KeyError):
d["nonexistent"]
class TestSafeDeepCopy:
def test_safe_deepcopy_dict(self):
original = {"key1": [1, 2, {"nested_key": "value"}], "key2": "simple_string"}
copied = safe_deepcopy(original)
assert copied == original
assert copied is not original
assert copied["key1"] is not original["key1"]
assert copied["key1"][2] is not original["key1"][2]
def test_safe_deepcopy_list(self):
original = [1, 2, [3, 4, {"key": "value"}]]
copied = safe_deepcopy(original)
assert copied == original
assert copied is not original
assert copied[2] is not original[2]
assert copied[2][2] is not original[2][2] # type: ignore
def test_safe_deepcopy_custom_object(self):
class CustomClass:
def __init__(self, value):
self.value = value
original = CustomClass(10)
copied = safe_deepcopy(original)
assert copied.value == original.value
assert copied is not original
def test_safe_deepcopy_handles_undeepcopyable(self):
class Uncopyable:
def __deepcopy__(self, memo):
raise TypeError("Can't deepcopy")
original = Uncopyable()
result = safe_deepcopy(original)
assert result is not original
assert type(result) is type(original)
def test_get_icon_path():
assert get_icon_path("plus.svg").endswith("plus.svg")
assert get_icon_path("huggingface-logo.svg").endswith("huggingface-logo.svg")
def test_error_payload():
result = error_payload(None, False)
assert result == {"error": None}
result = error_payload(Exception("test error"), True)
assert result == {"error": "test error", "visible": True}
gr_error = Error("custom error", duration=1.5, visible=True, title="Error Title")
result = error_payload(gr_error, False)
assert result == {
"error": "custom error",
"duration": 1.5,
"visible": True,
"title": "Error Title",
}
app_error = AppError("custom error")
result = error_payload(app_error, False)
assert result == {
"error": "custom error",
"duration": 10,
"visible": True,
"title": "Error",
}
class TestGetFunctionDescription:
def test_basic_function_description(self):
def test_func(param1, param2):
"""This is a test function.
Args:
param1: First parameter
param2: Second parameter
Returns:
- First return value
- Second return value
"""
pass
description, parameters, returns = get_function_description(test_func)
assert description == "This is a test function."
assert parameters == {"param1": "First parameter", "param2": "Second parameter"}
assert returns == ["- First return value", "- Second return value"]
def test_function_with_extended_returns(self):
def test_func(param1, param2):
"""This is a test function.
Args:
param1: First parameter
param2: Second parameter
Returns:
- First return value
- Second return value
Examples:
- Example 1
- Example 2
"""
pass
description, parameters, returns = get_function_description(test_func)
assert description == "This is a test function."
assert parameters == {"param1": "First parameter", "param2": "Second parameter"}
assert returns == [
"- First return value",
"- Second return value",
"Examples:",
"- Example 1",
"- Example 2",
]
def test_function_with_no_docstring(self):
def test_func():
pass
description, parameters, returns = get_function_description(test_func)
assert description == ""
assert parameters == {}
assert returns == []
def test_function_with_no_parameters(self):
def test_func():
"""This is a test function with no parameters."""
pass
description, parameters, returns = get_function_description(test_func)
assert description == "This is a test function with no parameters."
assert parameters == {}
assert returns == []
def test_function_with_alternate_parameter_section(self):
def test_func(param1, param2):
"""This is a test function.
Parameters:
param1: First parameter
param2: Second parameter
"""
pass
description, parameters, returns = get_function_description(test_func)
assert description == "This is a test function."
assert parameters == {"param1": "First parameter", "param2": "Second parameter"}
assert returns == []
def test_function_with_arguments_section(self):
def test_func(param1, param2):
"""This is a test function.
Arguments:
param1: First parameter
param2: Second parameter
Returns:
x: First return value
y: Second return value
"""
pass
description, parameters, returns = get_function_description(test_func)
assert description == "This is a test function."
assert parameters == {"param1": "First parameter", "param2": "Second parameter"}
assert returns == ["x: First return value", "y: Second return value"]
def test_function_with_multiline_description(self):
def test_func(param1, param2):
"""This is a test function.
It has multiple lines of description.
Args:
param1: First parameter
param2: Second parameter
"""
pass
description, parameters, returns = get_function_description(test_func)
assert (
description
== "This is a test function. It has multiple lines of description."
)
assert parameters == {"param1": "First parameter", "param2": "Second parameter"}
assert returns == []
def test_function_with_missing_params(self):
def test_func(param1, param2, param3):
"""This is a test function.
Args:
param1: description1
param3: description3
Returns:
- First return value
- Second return value
"""
pass
description, parameters, returns = get_function_description(test_func)
assert description == "This is a test function."
assert parameters == {
"param1": "description1",
"param2": "",
"param3": "description3",
}
assert returns == ["- First return value", "- Second return value"]
def test_function_with_nested_colons(self):
def test_func(param1, param2):
"""This is a test function.
Args:
param1: description1: with nested colon
param2: description2
"""
pass
description, parameters, returns = get_function_description(test_func)
assert description == "This is a test function."
assert parameters == {
"param1": "description1: with nested colon",
"param2": "description2",
}
assert returns == []
def test_get_function_description_inherits_parent_docstring(self):
"""
Test that get_function_description correctly retrieves docstrings
from a method's parent class if the method itself has none.
"""
class Parent:
def method(self):
"""This is the docstring from the parent class."""
pass
class Child(Parent):
def method(self):
pass # No docstring here
description, parameters, returns = get_function_description(Child().method)
assert parameters == {}
assert returns == []
assert description == "This is the docstring from the parent class."
class TestConnectHeartbeat:
def test_unload_registered_last(self):
with gr.Blocks() as demo:
msg = gr.Markdown("# Test")
gr.Button("Click").click(lambda: "# TADA!", outputs=[msg])
demo.unload(lambda: None)
config = demo.get_config_file()
assert config["connect_heartbeat"] is True
def test_unload_registered_first(self):
with gr.Blocks() as demo:
demo.unload(lambda: None)
msg = gr.Markdown("# Test")
gr.Button("Click").click(lambda: "# TADA!", outputs=[msg])
config = demo.get_config_file()
assert config["connect_heartbeat"] is True
def test_unload_registered_in_middle(self):
with gr.Blocks() as demo:
msg = gr.Markdown("# Test")
gr.Button("Click1").click(lambda: "# 1", outputs=[msg])
demo.unload(lambda: None)
gr.Button("Click2").click(lambda: "# 2", outputs=[msg])
config = demo.get_config_file()
assert config["connect_heartbeat"] is True
def test_no_unload_no_heartbeat(self):
with gr.Blocks() as demo:
msg = gr.Markdown("# Test")
gr.Button("Click").click(lambda: "# TADA!", outputs=[msg])
config = demo.get_config_file()
assert config["connect_heartbeat"] is False
def test_per_session_manual_cache_connects_heartbeat(self):
def predict(value, cache=gr.Cache(per_session=True)):
hit = cache.get(value)
if hit is not None:
return hit["value"]
cache.set(value, value=value)
return value
with gr.Blocks() as demo:
text = gr.Textbox()
output = gr.Textbox()
gr.Button("Click").click(predict, [text], [output])
config = demo.get_config_file()
assert config["connect_heartbeat"] is True
def test_per_session_decorator_cache_connects_heartbeat(self):
@gr.cache(per_session=True)
def predict(value):
return value
with gr.Blocks() as demo:
text = gr.Textbox()
output = gr.Textbox()
gr.Button("Click").click(predict, [text], [output])
config = demo.get_config_file()
assert config["connect_heartbeat"] is True
class _FakeIterator:
"""A fake iterator with a controllable close() for testing."""
def __init__(self, fail_count=0):
self.close_call_count = 0
self._fail_count = fail_count
self._closed = False
def close(self):
self.close_call_count += 1
if self.close_call_count <= self._fail_count:
raise ValueError("generator already executing")
self._closed = True
def __next__(self):
raise StopIteration
def __iter__(self):
return self
class TestSyncToAsyncIteratorAclose:
@pytest.mark.asyncio
async def test_aclose_closes_iterator(self):
"""aclose() should call close() on the underlying sync iterator."""
def gen():
yield 1
yield 2
iterator = SyncToAsyncIterator(gen(), limiter=None)
await iterator.aclose()
assert list(iterator.iterator) == []
@pytest.mark.asyncio
async def test_aclose_retries_on_already_executing(self):
"""aclose() should retry when ValueError('generator already executing') is raised."""
fake = _FakeIterator(fail_count=2)
iterator = SyncToAsyncIterator(fake, limiter=None)
await iterator.aclose(retry_interval=0.01)
assert fake.close_call_count == 3
assert fake._closed is True
@pytest.mark.asyncio
async def test_aclose_timeout_raises(self):
"""aclose() should raise ValueError after timeout is exceeded."""
fake = _FakeIterator(fail_count=999)
iterator = SyncToAsyncIterator(fake, limiter=None)
with pytest.raises(ValueError, match="already executing"):
await iterator.aclose(timeout=0.1, retry_interval=0.02)
@pytest.mark.asyncio
async def test_aclose_raises_other_value_error(self):
"""aclose() should not retry on ValueError without 'already executing'."""
class _BadIterator:
def close(self):
raise ValueError("some other error")
iterator = SyncToAsyncIterator(_BadIterator(), limiter=None)
with pytest.raises(ValueError, match="some other error"):
await iterator.aclose()
class TestSafeAcloseIterator:
@pytest.mark.asyncio
async def test_delegates_to_aclose(self):
"""safe_aclose_iterator() should call aclose() on the iterator."""
def gen():
yield 1
iterator = SyncToAsyncIterator(gen(), limiter=None)
await safe_aclose_iterator(iterator)
assert list(iterator.iterator) == []
@pytest.mark.asyncio
async def test_works_with_async_generator(self):
"""safe_aclose_iterator() should work with native async generators."""
async def gen():
yield 1
yield 2
ag = gen()
await safe_aclose_iterator(ag)
# After aclose, async generator should raise StopAsyncIteration
with pytest.raises(StopAsyncIteration):
await ag.__anext__()