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
335 lines
14 KiB
Python
335 lines
14 KiB
Python
import importlib.resources
|
|
import json
|
|
import tempfile
|
|
from copy import deepcopy
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Any, Literal, Optional, Union
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
from huggingface_hub import get_token
|
|
|
|
from gradio_client import utils
|
|
|
|
types = json.loads(importlib.resources.read_text("gradio_client", "types.json"))
|
|
types["MultipleFile"] = {
|
|
"type": "array",
|
|
"items": {"type": "string", "description": "filepath or URL to file"},
|
|
}
|
|
types["SingleFile"] = {"type": "string", "description": "filepath or URL to file"}
|
|
types["FileWithAdditionalProperties"] = {"type": "object", "additionalProperties": True}
|
|
HF_TOKEN = get_token()
|
|
|
|
|
|
class TestEnum(Enum):
|
|
VALUE1 = "option1"
|
|
VALUE2 = "option2"
|
|
VALUE3 = 42
|
|
|
|
|
|
def test_encode_url_or_file_to_base64(media_data):
|
|
output_base64 = utils.encode_url_or_file_to_base64(
|
|
Path(__file__).parents[3] / "gradio" / "test_data" / "test_image.png"
|
|
)
|
|
assert output_base64 == deepcopy(media_data.BASE64_IMAGE)
|
|
|
|
|
|
def test_encode_file_to_base64(media_data):
|
|
output_base64 = utils.encode_file_to_base64(
|
|
Path(__file__).parents[3] / "gradio" / "test_data" / "test_image.png"
|
|
)
|
|
assert output_base64 == deepcopy(media_data.BASE64_IMAGE)
|
|
|
|
|
|
@pytest.mark.flaky
|
|
def test_encode_url_to_base64(media_data):
|
|
output_base64 = utils.encode_url_to_base64(
|
|
"https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/test_data/test_image.png"
|
|
)
|
|
assert output_base64 == deepcopy(media_data.BASE64_IMAGE)
|
|
|
|
|
|
def test_encode_url_to_base64_doesnt_encode_errors(monkeypatch):
|
|
request = httpx.Request("GET", "https://example.com/foo")
|
|
error_response = httpx.Response(status_code=404, request=request)
|
|
monkeypatch.setattr(httpx, "get", lambda *args, **kwargs: error_response)
|
|
with pytest.raises(httpx.HTTPStatusError):
|
|
utils.encode_url_to_base64("https://example.com/foo")
|
|
|
|
|
|
def test_decode_base64_to_binary(media_data):
|
|
binary = utils.decode_base64_to_binary(deepcopy(media_data.BASE64_IMAGE))
|
|
assert deepcopy(media_data.BINARY_IMAGE) == binary
|
|
|
|
b64_img_without_header = deepcopy(media_data.BASE64_IMAGE).split(",")[1]
|
|
binary_without_header, extension = utils.decode_base64_to_binary(
|
|
b64_img_without_header
|
|
)
|
|
|
|
assert binary[0] == binary_without_header
|
|
assert extension is None
|
|
|
|
|
|
def test_decode_base64_to_file(media_data):
|
|
temp_file = utils.decode_base64_to_file(deepcopy(media_data.BASE64_IMAGE))
|
|
assert isinstance(temp_file, tempfile._TemporaryFileWrapper)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path_or_url, file_types, expected_result",
|
|
[
|
|
("/home/user/documents/example.pdf", [".json", "text", ".mp3", ".pdf"], True),
|
|
("C:\\Users\\user\\documents\\example.png", [".png"], True),
|
|
("C:\\Users\\user\\documents\\example.png", ["image"], True),
|
|
("C:\\Users\\user\\documents\\example.png", ["file"], True),
|
|
("/home/user/documents/example.pdf", [".json", "text", ".mp3"], False),
|
|
("https://example.com/avatar/xxxx.mp4", ["audio", ".png", ".jpg"], False),
|
|
# WebP support - case insensitive
|
|
("/home/user/images/photo.webp", ["image"], True),
|
|
("/home/user/images/photo.WEBP", ["image"], True),
|
|
("/home/user/images/photo.WebP", ["image"], True),
|
|
("C:\\Users\\user\\images\\photo.webp", ["image", "video"], True),
|
|
("C:\\Users\\user\\images\\photo.WEBP", ["image", "video"], True),
|
|
],
|
|
)
|
|
def test_is_valid_file_type(path_or_url, file_types, expected_result):
|
|
assert utils.is_valid_file(path_or_url, file_types) is expected_result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"filename, expected_mimetype",
|
|
[
|
|
("photo.webp", "image/webp"),
|
|
("photo.WEBP", "image/webp"),
|
|
("photo.WebP", "image/webp"),
|
|
("video.vtt", "text/vtt"),
|
|
("video.VTT", "text/vtt"),
|
|
("image.png", "image/png"),
|
|
],
|
|
)
|
|
def test_get_mimetype(filename, expected_mimetype):
|
|
assert utils.get_mimetype(filename) == expected_mimetype
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"orig_filename, new_filename",
|
|
[
|
|
("abc", "abc"),
|
|
("$$AAabc&3", "AAabc&3"),
|
|
("$$AAa&..b-c3_", "AAa&..b-c3_"),
|
|
("#.txt", "#.txt"),
|
|
("###.pdf", "###.pdf"),
|
|
("@!$.csv", "@.csv"),
|
|
("a#.txt", "a#.txt"),
|
|
# Path traversal characters are stripped
|
|
("a/b\\c.txt", "abc.txt"),
|
|
('a<b>c:"d.txt', "abcd.txt"),
|
|
("a\x00b.txt", "ab.txt"),
|
|
# Shell-dangerous characters ($, !, {, }) are stripped; parentheses and brackets preserved
|
|
("[{(Hunting's Shadowsl!)}].epub", "[(Hunting's Shadowsl)].epub"),
|
|
("l!)}]test[{(.txt", "l)]test[(.txt"),
|
|
(
|
|
"ゆかりです。私、こんなかわいい服は初めて着ました…。なんだかうれしくって、楽しいです。歌いたくなる気分って、初めてです。これがアイドルってことなのかもしれませんね",
|
|
"ゆかりです。私、こんなかわいい服は初めて着ました…。なんだかうれしくって、楽しいです。歌いたくなる気分って、初めてです。これがアイト",
|
|
),
|
|
(
|
|
"Bringing-computational-thinking-into-classrooms-a-systematic-review-on-supporting-teachers-in-integrating-computational-thinking-into-K12-classrooms_2024_Springer-Science-and-Business-Media-Deutschland-GmbH.pdf",
|
|
"Bringing-computational-thinking-into-classrooms-a-systematic-review-on-supporting-teachers-in-integrating-computational-thinking-into-K12-classrooms_2024_Springer-Science-and-Business-Media-Deutsc.pdf",
|
|
),
|
|
],
|
|
)
|
|
def test_strip_invalid_filename_characters(orig_filename, new_filename):
|
|
assert utils.strip_invalid_filename_characters(orig_filename) == new_filename
|
|
|
|
|
|
class AsyncMock(MagicMock):
|
|
async def __call__(self, *args, **kwargs):
|
|
return super().__call__(*args, **kwargs)
|
|
|
|
|
|
@patch("httpx.post")
|
|
def test_sleep_successful(mock_post):
|
|
utils.set_space_timeout("gradio/calculator")
|
|
|
|
|
|
@patch(
|
|
"httpx.post",
|
|
side_effect=httpx.HTTPStatusError("error", request=None, response=None),
|
|
)
|
|
def test_sleep_unsuccessful(mock_post):
|
|
with pytest.raises(utils.SpaceDuplicationError):
|
|
utils.set_space_timeout("gradio/calculator")
|
|
|
|
|
|
@pytest.mark.parametrize("schema", types)
|
|
def test_json_schema_to_python_type(schema):
|
|
if schema == "SimpleSerializable":
|
|
answer = "Any"
|
|
elif schema == "StringSerializable":
|
|
answer = "str"
|
|
elif schema == "ListStringSerializable":
|
|
answer = "list[str]"
|
|
elif schema == "BooleanSerializable":
|
|
answer = "bool"
|
|
elif schema == "NumberSerializable":
|
|
answer = "float"
|
|
elif schema == "ImgSerializable":
|
|
answer = "str"
|
|
elif schema == "FileSerializable":
|
|
answer = "str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file)) | list[str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))]"
|
|
elif schema == "JSONSerializable":
|
|
answer = "str | float | bool | list | dict"
|
|
elif schema == "GallerySerializable":
|
|
answer = "tuple[dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file)), str | None]"
|
|
elif schema == "SingleFileSerializable":
|
|
answer = "str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))"
|
|
elif schema == "MultipleFileSerializable":
|
|
answer = "list[str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))]"
|
|
elif schema == "SingleFile":
|
|
answer = "str"
|
|
elif schema == "MultipleFile":
|
|
answer = "list[str]"
|
|
elif schema == "FileWithAdditionalProperties":
|
|
answer = "dict(str, Any)"
|
|
else:
|
|
raise ValueError(f"This test has not been modified to check {schema}")
|
|
assert utils.json_schema_to_python_type(types[schema]) == answer
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"type_hint, expected_schema",
|
|
[
|
|
(str, {"type": "string"}),
|
|
(int, {"type": "integer"}),
|
|
(float, {"type": "number"}),
|
|
(bool, {"type": "boolean"}),
|
|
(type(None), {"type": "null"}),
|
|
(Any, {}),
|
|
(Union[str, int], {"anyOf": [{"type": "string"}, {"type": "integer"}]}),
|
|
(Optional[str], {"oneOf": [{"type": "null"}, {"type": "string"}]}),
|
|
(str | None, {"oneOf": [{"type": "null"}, {"type": "string"}]}),
|
|
(dict, {"type": "object", "additionalProperties": {}}),
|
|
(list, {"type": "array", "items": {}}),
|
|
(tuple, {"type": "array"}),
|
|
(set, {"type": "array", "uniqueItems": True}),
|
|
(frozenset, {"type": "array", "uniqueItems": True}),
|
|
(bytes, {"type": "string", "format": "byte"}),
|
|
(bytearray, {"type": "string", "format": "byte"}),
|
|
(TestEnum, {"enum": ["option1", "option2", 42]}),
|
|
],
|
|
)
|
|
def test_python_type_to_json_schema(type_hint, expected_schema):
|
|
schema = utils.python_type_to_json_schema(type_hint)
|
|
assert schema == expected_schema
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"type_hint, expected_schema",
|
|
[
|
|
(tuple[int, ...], {"type": "array", "items": {"type": "integer"}}),
|
|
(
|
|
tuple[str, int],
|
|
{
|
|
"type": "array",
|
|
"prefixItems": [{"type": "string"}, {"type": "integer"}],
|
|
"minItems": 2,
|
|
"maxItems": 2,
|
|
},
|
|
),
|
|
(set[str], {"type": "array", "uniqueItems": True, "items": {"type": "string"}}),
|
|
(
|
|
list[str | None],
|
|
{
|
|
"type": "array",
|
|
"items": {"oneOf": [{"type": "null"}, {"type": "string"}]},
|
|
},
|
|
),
|
|
(Literal["a", "b", "c"], {"enum": ["a", "b", "c"]}),
|
|
(Literal["single"], {"const": "single"}),
|
|
],
|
|
)
|
|
def test_python_type_to_json_schema_complex_nested_types(type_hint, expected_schema):
|
|
assert utils.python_type_to_json_schema(type_hint) == expected_schema
|
|
|
|
|
|
class TestConstructArgs:
|
|
def test_no_parameters_empty_args(self):
|
|
assert utils.construct_args(None, (), {}) == []
|
|
|
|
def test_no_parameters_with_args(self):
|
|
assert utils.construct_args(None, (1, 2), {}) == [1, 2]
|
|
|
|
def test_no_parameters_with_kwargs(self):
|
|
with pytest.raises(
|
|
ValueError, match="This endpoint does not support key-word arguments"
|
|
):
|
|
utils.construct_args(None, (), {"a": 1})
|
|
|
|
def test_parameters_no_args_kwargs(self):
|
|
parameters_info = [
|
|
{
|
|
"label": "param1",
|
|
"parameter_name": "a",
|
|
"parameter_has_default": True,
|
|
"parameter_default": 10,
|
|
}
|
|
]
|
|
assert utils.construct_args(parameters_info, (), {"a": 1}) == [1]
|
|
|
|
def test_parameters_with_args_no_kwargs(self):
|
|
parameters_info = [{"label": "param1", "parameter_name": "a"}]
|
|
assert utils.construct_args(parameters_info, (1,), {}) == [1]
|
|
|
|
def test_parameter_with_default_no_args_no_kwargs(self):
|
|
parameters_info = [
|
|
{"label": "param1", "parameter_has_default": True, "parameter_default": 10}
|
|
]
|
|
assert utils.construct_args(parameters_info, (), {}) == [10]
|
|
|
|
def test_args_filled_parameters_with_defaults(self):
|
|
parameters_info = [
|
|
{"label": "param1", "parameter_has_default": True, "parameter_default": 10},
|
|
{"label": "param2", "parameter_has_default": True, "parameter_default": 20},
|
|
]
|
|
assert utils.construct_args(parameters_info, (1,), {}) == [1, 20]
|
|
|
|
def test_kwargs_filled_parameters_with_defaults(self):
|
|
parameters_info = [
|
|
{
|
|
"label": "param1",
|
|
"parameter_name": "a",
|
|
"parameter_has_default": True,
|
|
"parameter_default": 10,
|
|
},
|
|
{
|
|
"label": "param2",
|
|
"parameter_name": "b",
|
|
"parameter_has_default": True,
|
|
"parameter_default": 20,
|
|
},
|
|
]
|
|
assert utils.construct_args(parameters_info, (), {"a": 1, "b": 2}) == [1, 2]
|
|
|
|
def test_positional_arg_and_kwarg_for_same_parameter(self):
|
|
parameters_info = [{"label": "param1", "parameter_name": "a"}]
|
|
with pytest.raises(
|
|
TypeError, match="Parameter `a` is already set as a positional argument."
|
|
):
|
|
utils.construct_args(parameters_info, (1,), {"a": 2})
|
|
|
|
def test_invalid_kwarg(self):
|
|
parameters_info = [{"label": "param1", "parameter_name": "a"}]
|
|
with pytest.raises(
|
|
TypeError, match="Parameter `b` is not a valid key-word argument."
|
|
):
|
|
utils.construct_args(parameters_info, (), {"b": 1})
|
|
|
|
def test_required_arg_missing(self):
|
|
parameters_info = [{"label": "param1", "parameter_name": "a"}]
|
|
with pytest.raises(
|
|
TypeError, match="No value provided for required argument: a"
|
|
):
|
|
utils.construct_args(parameters_info, (), {})
|