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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:32 +08:00
commit adf0d17497
3085 changed files with 456962 additions and 0 deletions
+515
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
abcdefghijklmnopqrstuvwxyz
File diff suppressed because one or more lines are too long
+5
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
from gradio_client import documentation
class TestDocumentation:
def test_website_documentation(self):
docs = documentation.generate_documentation()
assert len(docs) > 0
+114
View File
@@ -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
+334
View File
@@ -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, (), {})