chore: import upstream snapshot with attribution
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,515 @@
|
||||
import inspect
|
||||
import random
|
||||
import time
|
||||
|
||||
import gradio as gr
|
||||
import pytest
|
||||
from pydub import AudioSegment
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers", "flaky: mark test as flaky. Failure will not cause te"
|
||||
)
|
||||
config.addinivalue_line("markers", "serial: mark test as serial")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def calculator_demo():
|
||||
def calculator(num1, operation, num2):
|
||||
if operation == "add":
|
||||
return num1 + num2
|
||||
elif operation == "subtract":
|
||||
return num1 - num2
|
||||
elif operation == "multiply":
|
||||
return num1 * num2
|
||||
elif operation == "divide":
|
||||
if num2 == 0:
|
||||
raise gr.Error("Cannot divide by zero!")
|
||||
return num1 / num2
|
||||
|
||||
demo = gr.Interface(
|
||||
calculator,
|
||||
["number", gr.Radio(["add", "subtract", "multiply", "divide"]), "number"],
|
||||
"number",
|
||||
api_name="predict",
|
||||
examples=[
|
||||
[5, "add", 3],
|
||||
[4, "divide", 2],
|
||||
[-4, "multiply", 2.5],
|
||||
[0, "subtract", 1.2],
|
||||
],
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hello_world_demo():
|
||||
def greet(name, punctuation):
|
||||
return "Hello " + name + punctuation
|
||||
|
||||
demo = gr.Interface(
|
||||
fn=greet,
|
||||
inputs=[gr.Textbox(label="Name"), gr.Textbox(label="Punctuation")],
|
||||
outputs=gr.Textbox(label="Greeting"),
|
||||
api_name="greet",
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def calculator_demo_with_defaults():
|
||||
def calculator(num1, operation=None, num2=100):
|
||||
if operation is None or operation == "add":
|
||||
return num1 + num2
|
||||
elif operation == "subtract":
|
||||
return num1 - num2
|
||||
elif operation == "multiply":
|
||||
return num1 * num2
|
||||
elif operation == "divide":
|
||||
if num2 == 0:
|
||||
raise gr.Error("Cannot divide by zero!")
|
||||
return num1 / num2
|
||||
|
||||
demo = gr.Interface(
|
||||
calculator,
|
||||
[
|
||||
gr.Number(value=10),
|
||||
gr.Radio(["add", "subtract", "multiply", "divide"]),
|
||||
gr.Number(),
|
||||
],
|
||||
"number",
|
||||
examples=[
|
||||
[5, "add", 3],
|
||||
[4, "divide", 2],
|
||||
[-4, "multiply", 2.5],
|
||||
[0, "subtract", 1.2],
|
||||
],
|
||||
api_name="predict",
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def state_demo():
|
||||
state = gr.State(delete_callback=lambda x: print("STATE DELETED"))
|
||||
demo = gr.Interface(
|
||||
lambda x, y: (x, y),
|
||||
["textbox", state],
|
||||
["textbox", state],
|
||||
api_name="predict",
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def increment_demo():
|
||||
with gr.Blocks() as demo:
|
||||
btn1 = gr.Button("Increment")
|
||||
btn2 = gr.Button("Increment")
|
||||
btn3 = gr.Button("Increment")
|
||||
numb = gr.Number()
|
||||
|
||||
state = gr.State(0)
|
||||
|
||||
btn1.click(
|
||||
lambda x: (x + 1, x + 1),
|
||||
state,
|
||||
[state, numb],
|
||||
api_name="increment_with_queue",
|
||||
)
|
||||
btn2.click(
|
||||
lambda x: (x + 1, x + 1),
|
||||
state,
|
||||
[state, numb],
|
||||
queue=False,
|
||||
api_name="increment_without_queue",
|
||||
)
|
||||
btn3.click(
|
||||
lambda x: (x + 1, x + 1),
|
||||
state,
|
||||
[state, numb],
|
||||
api_visibility="private",
|
||||
)
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def progress_demo():
|
||||
def my_function(x, progress=gr.Progress()):
|
||||
progress(0, desc="Starting...")
|
||||
for _ in progress.tqdm(range(20)):
|
||||
time.sleep(0.1)
|
||||
return x
|
||||
|
||||
return gr.Interface(my_function, gr.Textbox(), gr.Textbox(), api_name="predict")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def yield_demo():
|
||||
def spell(x):
|
||||
for i in range(len(x)):
|
||||
time.sleep(0.5)
|
||||
yield x[:i]
|
||||
|
||||
return gr.Interface(spell, "textbox", "textbox", api_name="predict")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cancel_from_client_demo():
|
||||
def iteration():
|
||||
for i in range(20):
|
||||
print(f"i: {i}")
|
||||
yield i
|
||||
time.sleep(0.5)
|
||||
|
||||
def long_process():
|
||||
time.sleep(10)
|
||||
print("DONE!")
|
||||
return 10
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
num = gr.Number()
|
||||
|
||||
btn = gr.Button(value="Iterate")
|
||||
btn.click(iteration, None, num, api_name="iterate")
|
||||
btn2 = gr.Button(value="Long Process")
|
||||
btn2.click(long_process, None, num, api_name="long")
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sentiment_classification_demo():
|
||||
def classifier(text): # noqa: ARG001
|
||||
time.sleep(1)
|
||||
return {label: random.random() for label in ["POSITIVE", "NEGATIVE", "NEUTRAL"]}
|
||||
|
||||
def sleep_for_test():
|
||||
time.sleep(10)
|
||||
return 2
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
input_text = gr.Textbox(label="Input Text")
|
||||
with gr.Row():
|
||||
classify = gr.Button("Classify Sentiment")
|
||||
with gr.Column():
|
||||
label = gr.Label(label="Predicted Sentiment")
|
||||
number = gr.Number()
|
||||
btn = gr.Button("Sleep then print")
|
||||
classify.click(classifier, input_text, label, api_name="classify")
|
||||
btn.click(sleep_for_test, None, number, api_name="sleep")
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def count_generator_demo():
|
||||
def count(n):
|
||||
for i in range(int(n)):
|
||||
time.sleep(0.5)
|
||||
yield i
|
||||
|
||||
def show(n):
|
||||
return str(list(range(int(n))))
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Column():
|
||||
num = gr.Number(value=10)
|
||||
with gr.Row():
|
||||
count_btn = gr.Button("Count")
|
||||
list_btn = gr.Button("List")
|
||||
with gr.Column():
|
||||
out = gr.Textbox()
|
||||
|
||||
count_btn.click(count, num, out)
|
||||
list_btn.click(show, num, out)
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def count_generator_no_api():
|
||||
def count(n):
|
||||
for i in range(int(n)):
|
||||
time.sleep(0.5)
|
||||
yield i
|
||||
|
||||
def show(n):
|
||||
return str(list(range(int(n))))
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Column():
|
||||
num = gr.Number(value=10)
|
||||
with gr.Row():
|
||||
count_btn = gr.Button("Count")
|
||||
list_btn = gr.Button("List")
|
||||
with gr.Column():
|
||||
out = gr.Textbox()
|
||||
|
||||
count_btn.click(count, num, out, api_visibility="private")
|
||||
list_btn.click(show, num, out, api_visibility="private")
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def count_generator_demo_exception():
|
||||
def count(n):
|
||||
for i in range(int(n)):
|
||||
time.sleep(0.01)
|
||||
if i == 5:
|
||||
raise ValueError("Oh no!")
|
||||
yield i
|
||||
|
||||
def show(n):
|
||||
return str(list(range(int(n))))
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Column():
|
||||
num = gr.Number(value=10)
|
||||
with gr.Row():
|
||||
count_btn = gr.Button("Count")
|
||||
with gr.Column():
|
||||
out = gr.Textbox()
|
||||
|
||||
count_btn.click(count, num, out, api_name="count")
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def file_io_demo():
|
||||
demo = gr.Interface(
|
||||
lambda _: print("foox"),
|
||||
[gr.File(file_count="multiple"), "file"],
|
||||
[gr.File(file_count="multiple"), "file"],
|
||||
api_name="predict",
|
||||
)
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stateful_chatbot():
|
||||
with gr.Blocks() as demo:
|
||||
chatbot = gr.Chatbot()
|
||||
msg = gr.Textbox()
|
||||
clear = gr.Button("Clear")
|
||||
st = gr.State([1, 2, 3])
|
||||
|
||||
def respond(message, st, chat_history):
|
||||
assert st[0] == 1 and st[1] == 2 and st[2] == 3
|
||||
bot_message = "I love you"
|
||||
chat_history.append({"role": "user", "content": message})
|
||||
chat_history.append({"role": "assistant", "content": bot_message})
|
||||
return "", chat_history
|
||||
|
||||
msg.submit(respond, [msg, st, chatbot], [msg, chatbot], api_name="submit")
|
||||
clear.click(lambda: None, None, chatbot, queue=False)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hello_world_with_group():
|
||||
with gr.Blocks() as demo:
|
||||
name = gr.Textbox(label="name")
|
||||
output = gr.Textbox(label="greeting")
|
||||
greet = gr.Button("Greet")
|
||||
show_group = gr.Button("Show group")
|
||||
with gr.Group(visible=False) as group:
|
||||
gr.Textbox("Hello!")
|
||||
|
||||
def greeting(name):
|
||||
return f"Hello {name}", gr.Group(visible=True)
|
||||
|
||||
greet.click(
|
||||
greeting, inputs=[name], outputs=[output, group], api_name="greeting"
|
||||
)
|
||||
show_group.click(
|
||||
lambda: gr.Group(visible=False), None, group, api_name="show_group"
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hello_world_with_state_and_accordion():
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Row():
|
||||
name = gr.Textbox(label="name")
|
||||
output = gr.Textbox(label="greeting")
|
||||
num = gr.Number(label="count")
|
||||
with gr.Row():
|
||||
n_counts = gr.State(value=0)
|
||||
greet = gr.Button("Greet")
|
||||
open_acc = gr.Button("Open acc")
|
||||
close_acc = gr.Button("Close acc")
|
||||
with gr.Accordion(label="Extra stuff", open=False) as accordion:
|
||||
gr.Textbox("Hello!")
|
||||
|
||||
def greeting(name, state):
|
||||
"""This is a greeting function."""
|
||||
state += 1
|
||||
return state, f"Hello {name}", state, gr.Accordion(open=False)
|
||||
|
||||
greet.click(
|
||||
greeting,
|
||||
inputs=[name, n_counts],
|
||||
outputs=[n_counts, output, num, accordion],
|
||||
api_name="greeting",
|
||||
)
|
||||
open_acc.click(
|
||||
lambda state: (state + 1, state + 1, gr.Accordion(open=True)),
|
||||
[n_counts],
|
||||
[n_counts, num, accordion],
|
||||
api_name="open",
|
||||
)
|
||||
close_acc.click(
|
||||
lambda state: (state + 1, state + 1, gr.Accordion(open=False)),
|
||||
[n_counts],
|
||||
[n_counts, num, accordion],
|
||||
api_name="close",
|
||||
)
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stream_audio():
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
def _stream_audio(audio_file):
|
||||
audio = AudioSegment.from_mp3(audio_file)
|
||||
i = 0
|
||||
chunk_size = 3000
|
||||
|
||||
while chunk_size * i < len(audio):
|
||||
chunk = audio[chunk_size * i : chunk_size * (i + 1)]
|
||||
i += 1
|
||||
if chunk:
|
||||
file = str(pathlib.Path(tempfile.gettempdir()) / f"{i}.wav")
|
||||
chunk.export(file, format="wav")
|
||||
yield file
|
||||
|
||||
return gr.Interface(
|
||||
fn=_stream_audio,
|
||||
inputs=gr.Audio(type="filepath", label="Audio file to stream"),
|
||||
outputs=gr.Audio(autoplay=True, streaming=True),
|
||||
api_name="predict",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def video_component():
|
||||
return gr.Interface(
|
||||
fn=lambda x: x, inputs=gr.Video(), outputs=gr.Video(), api_name="predict"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def all_components():
|
||||
classes_to_check = gr.components.Component.__subclasses__()
|
||||
subclasses = []
|
||||
|
||||
while classes_to_check:
|
||||
subclass = classes_to_check.pop()
|
||||
children = subclass.__subclasses__()
|
||||
|
||||
if children:
|
||||
classes_to_check.extend(children)
|
||||
if (
|
||||
"value" in inspect.signature(subclass).parameters
|
||||
and subclass != gr.components.Component
|
||||
and not getattr(subclass, "is_template", False)
|
||||
):
|
||||
subclasses.append(subclass)
|
||||
|
||||
return subclasses
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def gradio_temp_dir(monkeypatch, tmp_path):
|
||||
"""tmp_path is unique to each test function.
|
||||
It will be cleared automatically according to pytest docs: https://docs.pytest.org/en/6.2.x/reference.html#tmp-path
|
||||
"""
|
||||
monkeypatch.setenv("GRADIO_TEMP_DIR", str(tmp_path))
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def long_response_with_info():
|
||||
def long_response(_):
|
||||
gr.Info("Beginning long response")
|
||||
time.sleep(17)
|
||||
gr.Info("Done!")
|
||||
return "\ta\nb" * 90000
|
||||
|
||||
return gr.Interface(
|
||||
long_response, None, gr.Textbox(label="Output"), api_name="predict"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def many_endpoint_demo():
|
||||
with gr.Blocks() as demo:
|
||||
|
||||
def noop(x):
|
||||
return x
|
||||
|
||||
n_elements = 1000
|
||||
for _ in range(n_elements):
|
||||
msg2 = gr.Textbox()
|
||||
msg2.submit(noop, msg2, msg2)
|
||||
butn2 = gr.Button()
|
||||
butn2.click(noop, msg2, msg2)
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def max_file_size_demo():
|
||||
with gr.Blocks() as demo:
|
||||
file_1b = gr.File()
|
||||
upload_status = gr.Textbox()
|
||||
|
||||
file_1b.upload(
|
||||
lambda x: "Upload successful", file_1b, upload_status, api_name="upload_1b"
|
||||
)
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chatbot_message_format():
|
||||
with gr.Blocks() as demo:
|
||||
chatbot = gr.Chatbot()
|
||||
msg = gr.Textbox()
|
||||
|
||||
def respond(message, chat_history: list):
|
||||
bot_message = random.choice(
|
||||
["How are you?", "I love you", "I'm very hungry"]
|
||||
)
|
||||
chat_history.extend(
|
||||
[
|
||||
{"role": "user", "content": message},
|
||||
{"role": "assistant", "content": bot_message},
|
||||
]
|
||||
)
|
||||
return "", chat_history
|
||||
|
||||
msg.submit(respond, [msg, chatbot], [msg, chatbot], api_name="chat")
|
||||
|
||||
return demo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def media_data():
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.append(Path(".").resolve().as_posix())
|
||||
from client.python.test import media_data
|
||||
|
||||
return media_data
|
||||
@@ -0,0 +1 @@
|
||||
abcdefghijklmnopqrstuvwxyz
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
pytest-asyncio
|
||||
pytest==7.1.2
|
||||
pytest-xdist
|
||||
ty==0.0.2
|
||||
pydub==0.25.1
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
from gradio_client import documentation
|
||||
|
||||
|
||||
class TestDocumentation:
|
||||
def test_website_documentation(self):
|
||||
docs = documentation.generate_documentation()
|
||||
assert len(docs) > 0
|
||||
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from contextlib import contextmanager
|
||||
|
||||
import gradio as gr
|
||||
|
||||
from gradio_client import Client
|
||||
from gradio_client.snippet import _stringify_py, generate_code_snippets
|
||||
|
||||
|
||||
@contextmanager
|
||||
def connect(demo: gr.Blocks, **kwargs):
|
||||
_, local_url, _ = demo.launch(prevent_thread_lock=True, **kwargs)
|
||||
try:
|
||||
yield Client(local_url)
|
||||
finally:
|
||||
demo.close()
|
||||
|
||||
|
||||
class TestSnippetExecution:
|
||||
def test_python_snippet_runs_for_simple_demo(self):
|
||||
def greet(name):
|
||||
return "Hello " + name + "!"
|
||||
|
||||
demo = gr.Interface(
|
||||
fn=greet,
|
||||
inputs=gr.Textbox(label="Name"),
|
||||
outputs=gr.Textbox(label="Greeting"),
|
||||
api_name="greet",
|
||||
)
|
||||
|
||||
with connect(demo) as client:
|
||||
api_info = client.view_api(print_info=False, return_format="dict")
|
||||
endpoint_info = api_info["named_endpoints"]["/greet"]
|
||||
snippets = generate_code_snippets("/greet", endpoint_info, client.src)
|
||||
|
||||
python_snippet = snippets["python"]
|
||||
assert "client.predict(" in python_snippet
|
||||
assert 'api_name="/greet"' in python_snippet
|
||||
|
||||
namespace = {}
|
||||
exec(python_snippet, namespace)
|
||||
assert namespace["result"] == "Hello Hello!!!"
|
||||
|
||||
def test_python_snippet_runs_for_calculator(self):
|
||||
def calculator(num1, operation, num2):
|
||||
if operation == "add":
|
||||
return num1 + num2
|
||||
elif operation == "subtract":
|
||||
return num1 - num2
|
||||
elif operation == "multiply":
|
||||
return num1 * num2
|
||||
elif operation == "divide":
|
||||
return num1 / num2
|
||||
|
||||
demo = gr.Interface(
|
||||
calculator,
|
||||
[
|
||||
"number",
|
||||
gr.Radio(["add", "subtract", "multiply", "divide"]),
|
||||
"number",
|
||||
],
|
||||
"number",
|
||||
api_name="predict",
|
||||
)
|
||||
|
||||
with connect(demo) as client:
|
||||
api_info = client.view_api(print_info=False, return_format="dict")
|
||||
endpoint_info = api_info["named_endpoints"]["/predict"]
|
||||
snippets = generate_code_snippets("/predict", endpoint_info, client.src)
|
||||
|
||||
python_snippet = snippets["python"]
|
||||
namespace = {}
|
||||
exec(python_snippet, namespace)
|
||||
assert namespace["result"] == 6.0
|
||||
|
||||
def test_python_snippet_runs_with_default_params(self):
|
||||
def add(a, b=10):
|
||||
return a + b
|
||||
|
||||
demo = gr.Interface(
|
||||
add,
|
||||
[gr.Number(label="a"), gr.Number(label="b", value=10)],
|
||||
gr.Number(label="result"),
|
||||
api_name="add",
|
||||
)
|
||||
|
||||
with connect(demo) as client:
|
||||
api_info = client.view_api(print_info=False, return_format="dict")
|
||||
endpoint_info = api_info["named_endpoints"]["/add"]
|
||||
snippets = generate_code_snippets("/add", endpoint_info, client.src)
|
||||
|
||||
python_snippet = snippets["python"]
|
||||
namespace = {}
|
||||
exec(python_snippet, namespace)
|
||||
assert isinstance(namespace["result"], (int, float))
|
||||
|
||||
|
||||
class TestStringifyPy:
|
||||
def test_datetime_in_nested_structure(self):
|
||||
"""Non-JSON-native types like datetime should not raise TypeError."""
|
||||
value = {
|
||||
"headers": ["t", "x"],
|
||||
"data": [[datetime.datetime(2026, 1, 1, 0, 0), 1]],
|
||||
}
|
||||
result = _stringify_py(value)
|
||||
assert "2026-01-01" in result
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_date_serialization(self):
|
||||
value = [datetime.date(2026, 6, 15)]
|
||||
result = _stringify_py(value)
|
||||
assert "2026-06-15" in result
|
||||
@@ -0,0 +1,334 @@
|
||||
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, (), {})
|
||||
Reference in New Issue
Block a user