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
File diff suppressed because it is too large Load Diff
+143
View File
@@ -0,0 +1,143 @@
# `gradio_client`: Use a Gradio app as an API -- in 3 lines of Python
This directory contains the source code for `gradio_client`, a lightweight Python library that makes it very easy to use any Gradio app as an API.
As an example, consider this [Hugging Face Space that transcribes audio files](https://huggingface.co/spaces/abidlabs/whisper) that are recorded from the microphone.
![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/whisper-screenshot.jpg)
Using the `gradio_client` library, we can easily use the Gradio as an API to transcribe audio files programmatically.
Here's the entire code to do it:
```python
from gradio_client import Client
client = Client("abidlabs/whisper")
client.predict("audio_sample.wav")
>> "This is a test of the whisper speech recognition model."
```
The Gradio client works with any Gradio Space, whether it be an image generator, a stateful chatbot, or a tax calculator.
## Installation
If you already have a recent version of `gradio`, then the `gradio_client` is included as a dependency.
Otherwise, the lightweight `gradio_client` package can be installed from pip (or pip3) and works with Python versions 3.10 or higher:
```bash
$ pip install gradio_client
```
## Basic Usage
### Connecting to a Space or a Gradio app
Start by connecting instantiating a `Client` object and connecting it to a Gradio app that is running on Spaces (or anywhere else)!
**Connecting to a Space**
```python
from gradio_client import Client
client = Client("abidlabs/en2fr") # a Space that translates from English to French
```
You can also connect to private Spaces by passing in your HF token with the `hf_token` parameter. You can get your HF token here: https://huggingface.co/settings/tokens
```python
from gradio_client import Client
client = Client("abidlabs/my-private-space", hf_token="...")
```
**Duplicating a Space for private use**
While you can use any public Space as an API, you may get rate limited by Hugging Face if you make too many requests. For unlimited usage of a Space, simply duplicate the Space to create a private Space,
and then use it to make as many requests as you'd like!
The `gradio_client` includes a class method: `Client.duplicate()` to make this process simple:
```python
from gradio_client import Client
client = Client.duplicate("abidlabs/whisper")
client.predict("audio_sample.wav")
>> "This is a test of the whisper speech recognition model."
```
If you have previously duplicated a Space, re-running `duplicate()` will _not_ create a new Space. Instead, the Client will attach to the previously-created Space. So it is safe to re-run the `Client.duplicate()` method multiple times.
**Note:** if the original Space uses GPUs, your private Space will as well, and your Hugging Face account will get billed based on the price of the GPU. To minimize charges, your Space will automatically go to sleep after 1 hour of inactivity. You can also set the hardware using the `hardware` parameter of `duplicate()`.
**Connecting a general Gradio app**
If your app is running somewhere else, just provide the full URL instead, including the "http://" or "https://". Here's an example of making predictions to a Gradio app that is running on a share URL:
```python
from gradio_client import Client
client = Client("https://bec81a83-5b5c-471e.gradio.live")
```
### Inspecting the API endpoints
Once you have connected to a Gradio app, you can view the APIs that are available to you by calling the `.view_api()` method. For the Whisper Space, we see the following:
```
Client.predict() Usage Info
---------------------------
Named API endpoints: 1
- predict(input_audio, api_name="/predict") -> value_0
Parameters:
- [Audio] input_audio: str (filepath or URL)
Returns:
- [Textbox] value_0: str (value)
```
This shows us that we have 1 API endpoint in this space, and shows us how to use the API endpoint to make a prediction: we should call the `.predict()` method, providing a parameter `input_audio` of type `str`, which is a `filepath or URL`.
We should also provide the `api_name='/predict'` argument. Although this isn't necessary if a Gradio app has a single named endpoint, it does allow us to call different endpoints in a single app if they are available. If an app has unnamed API endpoints, these can also be displayed by running `.view_api(all_endpoints=True)`.
### Making a prediction
The simplest way to make a prediction is simply to call the `.predict()` function with the appropriate arguments:
```python
from gradio_client import Client
client = Client("abidlabs/en2fr")
client.predict("Hello")
>> Bonjour
```
If there are multiple parameters, then you should pass them as separate arguments to `.predict()`, like this:
```python
from gradio_client import Client
client = Client("gradio/calculator")
client.predict(4, "add", 5)
>> 9.0
```
For certain inputs, such as images, you should pass in the filepath or URL to the file. Likewise, for the corresponding output types, you will get a filepath or URL returned.
```python
from gradio_client import Client
client = Client("abidlabs/whisper")
client.predict("https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3")
>> "My thought I have nobody by a beauty and will as you poured. Mr. Rochester is serve in that so don't find simpus, and devoted abode, to at might in a r—"
```
## Advanced Usage
For more ways to use the Gradio Python Client, check out our dedicated Guide on the Python client, available here: https://www.gradio.app/guides/getting-started-with-the-python-client
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
cd "$(dirname ${0})"
python3 -m pip install build
rm -rf dist/*
rm -rf build/*
python3 -m build
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
from gradio_client.client import Client
from gradio_client.data_classes import FileData
from gradio_client.utils import __version__, file, handle_file
__all__ = [
"Client",
"file",
"handle_file",
"FileData",
"__version__",
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
from __future__ import annotations
from typing import Any, TypedDict
from typing_extensions import NotRequired
class FileData(TypedDict):
name: str | None # filename
data: str | None # base64 encoded data
size: NotRequired[int | None] # size in bytes
is_file: NotRequired[
bool
] # whether the data corresponds to a file or base64 encoded data
orig_name: NotRequired[str] # original filename
mime_type: NotRequired[str]
is_stream: NotRequired[bool]
class ParameterInfo(TypedDict):
label: str
parameter_name: str
parameter_has_default: NotRequired[bool]
parameter_default: NotRequired[Any]
type: dict
python_type: dict
component: str
example_input: Any
@@ -0,0 +1,371 @@
"""Contains methods that generate documentation for Gradio functions and classes."""
from __future__ import annotations
import dataclasses
import inspect
import warnings
from collections import defaultdict
from collections.abc import Callable
from functools import lru_cache
classes_to_document = defaultdict(list)
classes_inherit_documentation = {}
def set_documentation_group(m): # noqa: ARG001
"""A no-op for backwards compatibility of custom components published prior to 4.16.0"""
pass
def extract_instance_attr_doc(cls, attr):
code = inspect.getsource(cls.__init__)
lines = [line.strip() for line in code.split("\n")]
i = None
for i, line in enumerate(lines): # noqa: B007
if line.startswith("self." + attr + ":") or line.startswith(
"self." + attr + " ="
):
break
if i is None:
raise NameError(f"Could not find {attr} in {cls.__name__}")
start_line = lines.index('"""', i)
end_line = lines.index('"""', start_line + 1)
for j in range(i + 1, start_line):
if lines[j].startswith("self."):
raise ValueError(
f"Found another attribute before docstring for {attr} in {cls.__name__}: "
+ lines[j]
+ "\n start:"
+ lines[i]
)
doc_string = " ".join(lines[start_line + 1 : end_line])
return doc_string
_module_prefixes = [
("gradio._simple_templates", "component"),
("gradio.server", "block"),
("gradio.block", "block"),
("gradio.chat", "chatinterface"),
("gradio.component", "component"),
("gradio.events", "helpers"),
("gradio.data_classes", "helpers"),
("gradio.exceptions", "helpers"),
("gradio.external", "helpers"),
("gradio.flag", "flagging"),
("gradio.helpers", "helpers"),
("gradio.interface", "interface"),
("gradio.layout", "layout"),
("gradio.route", "routes"),
("gradio.theme", "themes"),
("gradio_client.", "py-client"),
("gradio.utils", "helpers"),
("gradio.renderable", "renderable"),
("gradio.validators", "validators"),
("gradio.caching", "helpers"),
]
@lru_cache(maxsize=10)
def _get_module_documentation_group(modname) -> str:
for prefix, group in _module_prefixes:
if modname.startswith(prefix):
return group
raise ValueError(f"No known documentation group for module {modname!r}")
def document(*fns, inherit=False, documentation_group=None):
"""
Defines the @document decorator which adds classes or functions to the Gradio
documentation at www.gradio.app/docs.
Usage examples:
- Put @document() above a class to document the class and its constructor.
- Put @document("fn1", "fn2") above a class to also document methods fn1 and fn2.
- Put @document("*fn3") with an asterisk above a class to document the instance attribute methods f3.
"""
_documentation_group = documentation_group
def inner_doc(cls):
functions = list(fns)
if hasattr(cls, "EVENTS"):
functions += cls.EVENTS
if inherit:
classes_inherit_documentation[cls] = None
documentation_group = _documentation_group # avoid `nonlocal` reassignment
if _documentation_group is None:
try:
modname = inspect.getmodule(cls).__name__ # type: ignore
if modname.startswith("gradio.") or modname.startswith(
"gradio_client."
):
documentation_group = _get_module_documentation_group(modname)
else:
# Then this is likely a custom Gradio component that we do not include in the documentation
pass
except Exception as exc:
warnings.warn(f"Could not get documentation group for {cls}: {exc}")
classes_to_document[documentation_group].append((cls, functions))
return cls
return inner_doc
def document_fn(fn: Callable, cls) -> tuple[str, list[dict], dict, str | None]:
"""
Generates documentation for any function.
Parameters:
fn: Function to document
Returns:
description: General description of fn
parameters: A list of dicts for each parameter, storing data for the parameter name, annotation and doc
return: A dict storing data for the returned annotation and doc
example: Code for an example use of the fn
"""
doc_str = inspect.getdoc(fn) or ""
doc_lines = doc_str.split("\n")
signature = inspect.signature(fn)
description, parameters, returns, examples = [], {}, [], []
mode = "description"
current_parameter = None
base_indent = None
for line in doc_lines:
line = line.rstrip()
if line == "Parameters:":
mode = "parameter"
base_indent = None
elif line.startswith("Example:"):
mode = "example"
base_indent = None
if "(" in line and ")" in line:
c = line.split("(")[1].split(")")[0]
if c != cls.__name__:
mode = "ignore"
elif line == "Returns:":
mode = "return"
base_indent = None
else:
if mode == "description":
description.append(line if line.strip() else "<br>")
continue
if not (line.startswith(" ") or line.strip() == ""):
print(line)
if not (line.startswith(" ") or line.strip() == ""):
raise SyntaxError(
f"Documentation format for {fn.__name__} has format error in line: {line}"
)
line = line[4:]
if mode == "parameter":
if ": " in line and not line.startswith(" "):
colon_index = line.index(": ")
if colon_index < -1:
raise SyntaxError(
f"Documentation format for {fn.__name__} has format error in line: {line}"
)
current_parameter = line[:colon_index]
parameter_doc = line[colon_index + 2 :]
parameters[current_parameter] = parameter_doc
base_indent = None
elif current_parameter and line.strip():
if base_indent is None:
base_indent = len(line) - len(line.lstrip())
if base_indent > 0 and line.startswith(" " * base_indent):
line = line[base_indent:]
parameters[current_parameter] += "\n" + line
elif mode == "return":
returns.append(line)
elif mode == "example":
examples.append(line)
description_doc = " ".join(description)
parameter_docs = []
for param_name, param in signature.parameters.items():
if param_name.startswith("_"):
continue
if param_name == "self":
continue
if param_name in ["kwargs", "args"] and param_name not in parameters:
continue
parameter_doc = {
"name": param_name,
"annotation": param.annotation,
"doc": parameters.get(param_name),
}
if param_name in parameters:
del parameters[param_name]
if param.default != inspect.Parameter.empty:
default = param.default
if isinstance(default, str):
default = '"' + default + '"'
if default.__class__.__module__ != "builtins":
default = f"{default.__class__.__name__}()"
parameter_doc["default"] = default
elif parameter_doc["doc"] is not None:
if "kwargs" in parameter_doc["doc"]:
parameter_doc["kwargs"] = True
if "args" in parameter_doc["doc"]:
parameter_doc["args"] = True
parameter_docs.append(parameter_doc)
if parameters:
raise ValueError(
f"Documentation format for {fn.__name__} documents "
f"nonexistent parameters: {', '.join(parameters.keys())}. "
f"Valid parameters: {', '.join(signature.parameters.keys())}"
)
if len(returns) == 0:
return_docs = {}
else:
return_doc_text = "\n".join(returns)
return_docs = {
"annotation": signature.return_annotation,
"doc": return_doc_text,
}
examples_doc = "\n".join(examples) if len(examples) > 0 else None
return description_doc, parameter_docs, return_docs, examples_doc
def document_cls(cls):
doc_str = inspect.getdoc(cls)
if doc_str is None:
return "", {}, ""
tags = {}
description_lines = []
mode = "description"
for line in doc_str.split("\n"):
line = line.rstrip()
if line.endswith(":") and " " not in line:
mode = line[:-1].lower()
tags[mode] = []
elif line.split(" ")[0].endswith(":") and not line.startswith(" "):
tag = line[: line.index(":")].lower()
value = line[line.index(":") + 2 :]
tags[tag] = value
elif mode == "description":
description_lines.append(line if line.strip() else "<br>")
else:
if not (line.startswith(" ") or not line.strip()):
raise SyntaxError(
f"Documentation format for {cls.__name__} has format error in line: {line}"
)
tags[mode].append(line[4:])
if "example" in tags:
example = "\n".join(tags["example"])
del tags["example"]
else:
example = None
for key, val in tags.items():
if isinstance(val, list):
tags[key] = "<br>".join(val)
description = " ".join(description_lines).replace("\n", "<br>")
return description, tags, example
def generate_documentation():
documentation = {}
for mode, class_list in classes_to_document.items():
documentation[mode] = []
for cls, fns in class_list:
fn_to_document = (
cls
if inspect.isfunction(cls) or dataclasses.is_dataclass(cls)
else cls.__init__
)
_, parameter_doc, return_doc, _ = document_fn(fn_to_document, cls)
if (
hasattr(cls, "preprocess")
and callable(cls.preprocess) # type: ignore
and hasattr(cls, "postprocess")
and callable(cls.postprocess) # type: ignore
):
preprocess_doc = document_fn(cls.preprocess, cls) # type: ignore
postprocess_doc = document_fn(cls.postprocess, cls) # type: ignore
preprocess_doc, postprocess_doc = (
{
"parameter_doc": preprocess_doc[1],
"return_doc": preprocess_doc[2],
},
{
"parameter_doc": postprocess_doc[1],
"return_doc": postprocess_doc[2],
},
)
cls_description, cls_tags, cls_example = document_cls(cls)
cls_documentation = {
"class": cls,
"name": cls.__name__,
"description": cls_description,
"tags": cls_tags,
"parameters": parameter_doc,
"returns": return_doc,
"example": cls_example,
"fns": [],
}
if (
hasattr(cls, "preprocess")
and callable(cls.preprocess) # type: ignore
and hasattr(cls, "postprocess")
and callable(cls.postprocess) # type: ignore
):
cls_documentation["preprocess"] = preprocess_doc # type: ignore
cls_documentation["postprocess"] = postprocess_doc # type: ignore
for fn_name in fns:
instance_attribute_fn = fn_name.startswith("*")
if instance_attribute_fn:
fn_name = fn_name[1:]
# Instance attribute fns are classes
# whose __call__ method determines their behavior
fn = getattr(cls(), fn_name).__call__
else:
fn = getattr(cls, fn_name)
if not callable(fn):
description_doc = str(fn)
parameter_docs = {}
return_docs = {}
examples_doc = ""
override_signature = f"gr.{cls.__name__}.{fn_name}"
else:
(
description_doc,
parameter_docs,
return_docs,
examples_doc,
) = document_fn(fn, cls)
if fn_name in getattr(cls, "EVENTS", []):
parameter_docs = parameter_docs[1:]
override_signature = None
if instance_attribute_fn:
description_doc = extract_instance_attr_doc(cls, fn_name)
cls_documentation["fns"].append(
{
"fn": fn,
"name": fn_name,
"description": description_doc,
"tags": {},
"parameters": parameter_docs,
"returns": return_docs,
"example": examples_doc,
"override_signature": override_signature,
}
)
documentation[mode].append(cls_documentation)
if cls in classes_inherit_documentation:
classes_inherit_documentation[cls] = cls_documentation["fns"]
for mode, class_list in classes_to_document.items():
for i, (cls, _) in enumerate(class_list):
for super_class, fns in classes_inherit_documentation.items():
if (
inspect.isclass(cls)
and issubclass(cls, super_class)
and cls != super_class
):
for inherited_fn in fns:
inherited_fn = dict(inherited_fn)
try:
inherited_fn["description"] = extract_instance_attr_doc(
cls, inherited_fn["name"]
)
except ValueError:
pass
documentation[mode][i]["fns"].append(inherited_fn)
return documentation
+43
View File
@@ -0,0 +1,43 @@
class SerializationSetupError(ValueError):
"""Raised when a serializers cannot be set up correctly."""
pass
class AuthenticationError(ValueError):
"""Raised when the client is unable to authenticate itself to a Gradio app due to invalid or missing credentials."""
pass
class ValidationError(ValueError):
"""Raised when the data that is passed into the Gradio app fails developer-defined validation."""
pass
class AppError(ValueError):
"""Raised when the upstream Gradio app throws an error because of the value submitted by the client."""
def __init__(
self,
message: str = "Error raised.",
duration: float | None = 10,
visible: bool = True,
title: str = "Error",
print_exception: bool = True,
):
"""
Parameters:
message: The error message to be displayed to the user. Can be HTML, which will be rendered in the modal.
duration: The duration in seconds to display the error message. If None or 0, the error message will be displayed until the user closes it.
visible: Whether the error message should be displayed in the UI.
title: The title to be displayed to the user at the top of the error modal.
print_exception: Whether to print traceback of the error to the console when the error is raised.
"""
self.title = title
self.message = message
self.duration = duration
self.visible = visible
self.print_exception = print_exception
super().__init__(self.message)
+8
View File
@@ -0,0 +1,8 @@
{
"name": "gradio_client",
"version": "2.5.0",
"description": "",
"python": "true",
"main_changeset": true,
"private": true
}
+276
View File
@@ -0,0 +1,276 @@
"""Centralized code snippet generation for Gradio API endpoints. Generates Python, JavaScript, and cURL code snippets from API info dicts."""
import copy
import json
import re
from typing import Any
BLOB_COMPONENTS = {
"Audio",
"DownloadButton",
"File",
"Image",
"ImageSlider",
"Model3D",
"UploadButton",
"Video",
}
def _is_file_data(obj: Any) -> bool:
return (
isinstance(obj, dict)
and "url" in obj
and obj.get("url")
and "meta" in obj
and isinstance(obj.get("meta"), dict)
and obj["meta"].get("_type") == "gradio.FileData"
)
def _has_file_data(obj: Any) -> bool:
if isinstance(obj, dict):
if _is_file_data(obj):
return True
return any(_has_file_data(v) for v in obj.values())
if isinstance(obj, (list, tuple)):
return any(_has_file_data(item) for item in obj)
return False
def _replace_file_data_py(obj: Any) -> Any:
if isinstance(obj, dict) and _is_file_data(obj):
return f"handle_file('{obj['url']}')"
if isinstance(obj, (list, tuple)):
return [_replace_file_data_py(item) for item in obj]
if isinstance(obj, dict):
return {k: _replace_file_data_py(v) for k, v in obj.items()}
return obj
def _simplify_file_data(obj: Any) -> Any:
if isinstance(obj, dict) and _is_file_data(obj):
return {"path": obj["url"], "meta": {"_type": "gradio.FileData"}}
if isinstance(obj, (list, tuple)):
return [_simplify_file_data(item) for item in obj]
if isinstance(obj, dict):
return {k: _simplify_file_data(v) for k, v in obj.items()}
return obj
_UNQUOTED = "UNQUOTED_GRADIO_"
def _stringify_py(obj: Any) -> str:
def _prepare(o: Any) -> Any:
if o is None:
return f"{_UNQUOTED}None"
if isinstance(o, bool):
return f"{_UNQUOTED}True" if o else f"{_UNQUOTED}False"
if isinstance(o, str) and o.startswith("handle_file(") and o.endswith(")"):
return f"{_UNQUOTED}{o}"
if isinstance(o, (list, tuple)):
return [_prepare(item) for item in o]
if isinstance(o, dict):
return {k: _prepare(v) for k, v in o.items()}
return o
prepared = _prepare(obj)
result = json.dumps(prepared, default=str)
result = re.sub(
rf'"{_UNQUOTED}(handle_file\([^)]*\))"',
r"\1",
result,
)
result = result.replace(f'"{_UNQUOTED}None"', "None")
result = result.replace(f'"{_UNQUOTED}True"', "True")
result = result.replace(f'"{_UNQUOTED}False"', "False")
return result
def _represent_value(value: Any, python_type: str | None, lang: str) -> str:
if python_type is None:
return "None" if lang == "py" else "null"
if value is None:
return "None" if lang == "py" else "null"
if python_type in ("string", "str"):
return f'"{value}"'
if python_type == "number":
return str(value)
if python_type in ("boolean", "bool"):
if lang == "py":
return "True" if value else "False"
return str(value).lower() if isinstance(value, bool) else str(value)
if python_type == "List[str]":
return json.dumps(value)
if python_type.startswith("Literal['"):
return f'"{value}"'
if isinstance(value, str):
if value == "":
return "None" if lang == "py" else "null"
return value
value = copy.deepcopy(value)
if lang == "bash":
value = _simplify_file_data(value)
if lang == "py":
value = _replace_file_data_py(value)
return _stringify_py(value)
def _get_param_value(param: dict) -> Any:
if param.get("parameter_has_default"):
return param.get("parameter_default")
return param.get("example_input")
def generate_python_snippet(
api_name: str,
params: list[dict],
src: str,
) -> str:
has_file = any(_has_file_data(p.get("example_input")) for p in params)
imports = "from gradio_client import Client"
if has_file:
imports += ", handle_file"
lines = [imports, ""]
lines.append(f'client = Client("{src}")')
predict_args = []
for p in params:
name = p.get("parameter_name") or p.get("label", "input")
value = _get_param_value(p)
ptype = p.get("python_type", {}).get("type")
formatted = _represent_value(value, ptype, "py")
predict_args.append(f"\t{name}={formatted},")
lines.append("result = client.predict(")
lines.extend(predict_args)
lines.append(f'\tapi_name="{api_name}",')
lines.append(")")
lines.append("print(result)")
return "\n".join(lines)
def generate_js_snippet(
api_name: str,
params: list[dict],
src: str,
) -> str:
blob_params = [p for p in params if p.get("component") in BLOB_COMPONENTS]
lines = ['import { Client } from "@gradio/client";', ""]
for i, bp in enumerate(blob_params):
example = bp.get("example_input", {})
url = example.get("url", "") if isinstance(example, dict) else ""
component = bp.get("component", "")
lines.append(f'const response_{i} = await fetch("{url}");')
lines.append(f"const example{component} = await response_{i}.blob();")
if blob_params:
lines.append("")
lines.append(f'const client = await Client.connect("{src}");')
blob_component_names = {bp.get("component") for bp in blob_params}
predict_args = []
for p in params:
name = p.get("parameter_name") or p.get("label", "input")
component = p.get("component", "")
if component in blob_component_names:
predict_args.append(f"\t\t{name}: example{component},")
else:
value = _get_param_value(p)
ptype = p.get("python_type", {}).get("type")
formatted = _represent_value(value, ptype, "js")
predict_args.append(f"\t\t{name}: {formatted},")
lines.append(f'const result = await client.predict("{api_name}", {{')
lines.extend(predict_args)
lines.append("});")
lines.append("")
lines.append("console.log(result.data);")
return "\n".join(lines)
def generate_bash_snippet(
api_name: str,
params: list[dict],
root: str,
api_prefix: str = "/",
) -> str:
normalised_root = root.rstrip("/")
normalised_prefix = api_prefix if api_prefix else "/"
endpoint_name = api_name.lstrip("/")
has_file = any(_has_file_data(p.get("example_input")) for p in params)
upload_url = f"{normalised_root}{normalised_prefix}upload"
lines: list[str] = []
file_param_names: list[str] = []
if has_file:
for p in params:
if _has_file_data(p.get("example_input")):
name = p.get("parameter_name") or p.get("label", "input")
file_param_names.append(name)
lines.append(
f"FILE_PATH=$(curl -s -X POST {upload_url}"
" -F 'files=@/path/to/your/file'"
" | tr -d '[]\" ')"
)
lines.append("")
data_dict = {}
for p in params:
name = p.get("parameter_name") or p.get("label", "input")
if name in file_param_names:
data_dict[name] = "FILE_PATH_PLACEHOLDER"
else:
value = _get_param_value(p)
ptype = p.get("python_type", {}).get("type")
formatted = _represent_value(value, ptype, "bash")
data_dict[name] = formatted
data_entries = ", ".join(f'"{k}": {v}' for k, v in data_dict.items())
data_str = "{" + data_entries + "}"
for _ in file_param_names:
replacement = '{"path": "\'$FILE_PATH\'", "meta": {"_type": "gradio.FileData"}}'
data_str = data_str.replace("FILE_PATH_PLACEHOLDER", replacement)
base_url = f"{normalised_root}{normalised_prefix}call/v2/{endpoint_name}"
get_url = f"{normalised_root}{normalised_prefix}call/{endpoint_name}"
lines.extend(
[
f'curl -X POST {base_url} -s -H "Content-Type: application/json" \\',
f" -d '{data_str}' \\",
" | awk -F'\"' '{ print $4}' \\",
f" | read EVENT_ID; curl -N {get_url}/$EVENT_ID",
]
)
return "\n".join(lines)
def generate_code_snippets(
api_name: str,
endpoint_info: dict,
root: str,
space_id: str | None = None,
api_prefix: str = "/",
) -> dict[str, str]:
params = endpoint_info.get("parameters", [])
src = space_id or root
return {
"python": generate_python_snippet(api_name, params, src),
"javascript": generate_js_snippet(api_name, params, src),
"bash": generate_bash_snippet(api_name, params, root, api_prefix),
}
@@ -0,0 +1,192 @@
import asyncio
import os
import threading
from threading import Event
import discord
import gradio as gr
from discord import Permissions
from discord.ext import commands
from discord.utils import oauth_url
import gradio_client as grc
from gradio_client.utils import QueueError
event = Event()
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
async def wait(job):
while not job.done():
await asyncio.sleep(0.2)
def get_client(session: str | None = None) -> grc.Client:
client = grc.Client("<<app-src>>", token=os.getenv("HF_TOKEN"))
if session:
client.session_hash = session
return client
def truncate_response(response: str) -> str:
ending = "...\nTruncating response to 2000 characters due to discord api limits."
if len(response) > 2000:
return response[: 2000 - len(ending)] + ending
else:
return response
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="/", intents=intents)
@bot.event
async def on_ready():
print(f"Logged in as {bot.user} (ID: {bot.user.id})")
synced = await bot.tree.sync()
print(f"Synced commands: {', '.join([s.name for s in synced])}.")
event.set()
print("------")
thread_to_client = {}
thread_to_user = {}
@bot.hybrid_command(
name="<<command-name>>",
description="Enter some text to chat with the bot! Like this: /<<command-name>> Hello, how are you?",
)
async def chat(ctx, prompt: str):
if ctx.author.id == bot.user.id:
return
try:
message = await ctx.send("Creating thread...")
thread = await message.create_thread(name=prompt)
loop = asyncio.get_running_loop()
client = await loop.run_in_executor(None, get_client, None)
job = client.submit(prompt, api_name="/<<api-name>>")
await wait(job)
try:
job.result()
response = job.outputs()[-1]
await thread.send(truncate_response(response))
thread_to_client[thread.id] = client
thread_to_user[thread.id] = ctx.author.id
except QueueError:
await thread.send(
"The gradio space powering this bot is really busy! Please try again later!"
)
except Exception as e:
print(f"{e}")
async def continue_chat(message):
"""Continues a given conversation based on chathistory"""
try:
client = thread_to_client[message.channel.id]
prompt = message.content
job = client.submit(prompt, api_name="/<<api-name>>")
await wait(job)
try:
job.result()
response = job.outputs()[-1]
await message.reply(truncate_response(response))
except QueueError:
await message.reply(
"The gradio space powering this bot is really busy! Please try again later!"
)
except Exception as e:
print(f"Error: {e}")
@bot.event
async def on_message(message):
"""Continue the chat"""
try:
if not message.author.bot:
if message.channel.id in thread_to_user:
if thread_to_user[message.channel.id] == message.author.id:
await continue_chat(message)
else:
await bot.process_commands(message)
except Exception as e:
print(f"Error: {e}")
# running in thread
def run_bot():
if not DISCORD_TOKEN:
print("DISCORD_TOKEN NOT SET")
event.set()
else:
bot.run(DISCORD_TOKEN)
threading.Thread(target=run_bot).start()
event.wait()
if not DISCORD_TOKEN:
welcome_message = """
## You have not specified a DISCORD_TOKEN, which means you have not created a bot account. Please follow these steps:
### 1. Go to https://discord.com/developers/applications and click 'New Application'
### 2. Give your bot a name 🤖
![](https://gradio-builds.s3.amazonaws.com/demo-files/discordbots/BotName.png)
## 3. In Settings > Bot, click the 'Reset Token' button to get a new token. Write it down and keep it safe 🔐
![](https://gradio-builds.s3.amazonaws.com/demo-files/discordbots/ResetToken.png)
## 4. Optionally make the bot public if you want anyone to be able to add it to their servers
## 5. Scroll down and enable 'Message Content Intent' under 'Priviledged Gateway Intents'
![](https://gradio-builds.s3.amazonaws.com/demo-files/discordbots/MessageContentIntent.png)
## 6. Save your changes!
## 7. The token from step 3 is the DISCORD_TOKEN. Rerun the deploy_discord command, e.g client.deploy_discord(discord_bot_token=DISCORD_TOKEN, ...), or add the token as a space secret manually.
"""
else:
permissions = Permissions(326417525824)
url = oauth_url(bot.user.id, permissions=permissions)
welcome_message = f"""
## Add this bot to your server by clicking this link:
{url}
## How to use it?
The bot can be triggered via `/<<command-name>>` followed by your text prompt.
This will create a thread with the bot's response to your text prompt.
You can reply in the thread (without `/<<command-name>>`) to continue the conversation.
In the thread, the bot will only reply to the original author of the command.
⚠️ Note ⚠️: Please make sure this bot's command does have the same name as another command in your server.
⚠️ Note ⚠️: Bot commands do not work in DMs with the bot as of now.
"""
with gr.Blocks() as demo:
gr.Markdown(
f"""
# Discord bot of <<app-src>>
{welcome_message}
"""
)
demo.launch()
+199
View File
@@ -0,0 +1,199 @@
{
"SimpleSerializable": {
"type": {},
"description": "any valid value"
},
"StringSerializable": {
"type": "string"
},
"ListStringSerializable": {
"type": "array",
"items": {
"type": "string"
}
},
"BooleanSerializable": {
"type": "boolean"
},
"NumberSerializable": {
"type": "number"
},
"ImgSerializable": {
"type": "string",
"description": "base64 representation of an image"
},
"FileSerializable": {
"oneOf": [
{
"type": "string",
"description": "filepath on your computer (or URL) of file"
},
{
"type": "object",
"properties": {
"name": { "type": "string", "description": "name of file" },
"data": {
"type": "string",
"description": "base64 representation of file"
},
"size": {
"type": "integer",
"description": "size of image in bytes"
},
"is_file": {
"type": "boolean",
"description": "true if the file has been uploaded to the server"
},
"orig_name": {
"type": "string",
"description": "original name of the file"
}
},
"required": ["name", "data"]
},
{
"type": "array",
"items": {
"anyOf": [
{
"type": "string",
"description": "filepath on your computer (or URL) of file"
},
{
"type": "object",
"properties": {
"name": { "type": "string", "description": "name of file" },
"data": {
"type": "string",
"description": "base64 representation of file"
},
"size": {
"type": "integer",
"description": "size of image in bytes"
},
"is_file": {
"type": "boolean",
"description": "true if the file has been uploaded to the server"
},
"orig_name": {
"type": "string",
"description": "original name of the file"
}
},
"required": ["name", "data"]
}
]
}
}
]
},
"SingleFileSerializable": {
"oneOf": [
{
"type": "string",
"description": "filepath on your computer (or URL) of file"
},
{
"type": "object",
"properties": {
"name": { "type": "string", "description": "name of file" },
"data": {
"type": "string",
"description": "base64 representation of file"
},
"size": {
"type": "integer",
"description": "size of image in bytes"
},
"is_file": {
"type": "boolean",
"description": "true if the file has been uploaded to the server"
},
"orig_name": {
"type": "string",
"description": "original name of the file"
}
},
"required": ["name", "data"]
}
]
},
"MultipleFileSerializable": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string",
"description": "filepath on your computer (or URL) of file"
},
{
"type": "object",
"properties": {
"name": { "type": "string", "description": "name of file" },
"data": {
"type": "string",
"description": "base64 representation of file"
},
"size": {
"type": "integer",
"description": "size of image in bytes"
},
"is_file": {
"type": "boolean",
"description": "true if the file has been uploaded to the server"
},
"orig_name": {
"type": "string",
"description": "original name of the file"
}
},
"required": ["name", "data"]
}
]
}
},
"JSONSerializable": {
"type": {},
"description": "any valid json"
},
"GallerySerializable": {
"type": "array",
"items": {
"type": "array",
"items": false,
"maxSize": 2,
"minSize": 2,
"prefixItems": [
{
"type": "object",
"properties": {
"name": { "type": "string", "description": "name of file" },
"data": {
"type": "string",
"description": "base64 representation of file"
},
"size": {
"type": "integer",
"description": "size of image in bytes"
},
"is_file": {
"type": "boolean",
"description": "true if the file has been uploaded to the server"
},
"orig_name": {
"type": "string",
"description": "original name of the file"
}
},
"required": ["name", "data"]
},
{
"oneOf": [
{ "type": "string", "description": "caption of image" },
{ "type": "null" }
]
}
]
}
}
}
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
[build-system]
requires = ["hatchling", "hatch-requirements-txt", "hatch-fancy-pypi-readme>=22.5.0"]
build-backend = "hatchling.build"
[project]
name = "gradio_client"
dynamic = ["version", "dependencies", "readme"]
description = "Python library for easily interacting with trained machine learning models"
license = "Apache-2.0"
requires-python = ">=3.10"
authors = [
{ name = "Abubakar Abid", email = "gradio-team@huggingface.co" },
{ name = "Ali Abid", email = "gradio-team@huggingface.co" },
{ name = "Ali Abdalla", email = "gradio-team@huggingface.co" },
{ name = "Dawood Khan", email = "gradio-team@huggingface.co" },
{ name = "Ahsen Khaliq", email = "gradio-team@huggingface.co" },
{ name = "Pete Allen", email = "gradio-team@huggingface.co" },
{ name = "Freddy Boulton", email = "gradio-team@huggingface.co" },
]
keywords = ["machine learning", "client", "API"]
classifiers = [
'Development Status :: 4 - Beta',
'License :: OSI Approved :: Apache Software License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Topic :: Scientific/Engineering',
'Topic :: Scientific/Engineering :: Artificial Intelligence',
'Topic :: Software Development :: User Interfaces',
]
[project.urls]
Homepage = "https://github.com/gradio-app/gradio"
[tool.hatch.version]
path = "gradio_client/package.json"
pattern = ".*\"version\":\\s*\"(?P<version>[^\"]+)\""
[tool.hatch.metadata.hooks.requirements_txt]
filename = "requirements.txt"
[tool.hatch.metadata.hooks.fancy-pypi-readme]
content-type = "text/markdown"
fragments = [
{ path = "README.md" },
]
[tool.hatch.build.targets.sdist]
include = [
"/gradio_client",
"/README.md",
"/requirements.txt",
]
[tool.ruff]
extend = "../../pyproject.toml"
[tool.ruff.lint.isort]
known-first-party = [
"gradio_client"
]
[tool.pytest.ini_options]
GRADIO_ANALYTICS_ENABLED = "False"
HF_HUB_DISABLE_TELEMETRY = "1"
+5
View File
@@ -0,0 +1,5 @@
fsspec
httpx>=0.24.1
huggingface_hub>=0.19.3,<2.0
packaging
typing_extensions~=4.0
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash -eu
cd "$(dirname ${0})/.."
echo "Testing..."
python -m pytest test
+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, (), {})