diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd67cd8257..c05c38aa3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,6 +73,9 @@ Do not forget the format the backend before pushing. ``` bash scripts/format_backend.sh ``` +``` +bash scripts/format_frontend.sh +``` You can run the circleci checks locally as well. ``` bash scripts/run_circleci.sh diff --git a/gradio/component.py b/gradio/component.py index 7494a20f49..9a9ef295d3 100644 --- a/gradio/component.py +++ b/gradio/component.py @@ -1,6 +1,6 @@ import os import shutil -from typing import Any, Dict +from typing import Any, Dict, Optional from gradio import processing_utils @@ -49,7 +49,7 @@ class Component: def save_flagged_file( self, dir: str, label: str, data: Any, encryption_key: bool - ) -> str: + ) -> Optional[str]: """ Saved flagged data (e.g. image or audio) as a file and returns filepath """ diff --git a/gradio/inputs.py b/gradio/inputs.py index 3cff47e6b1..e528a3481b 100644 --- a/gradio/inputs.py +++ b/gradio/inputs.py @@ -30,11 +30,14 @@ class InputComponent(Component): Input Component. All input components subclass this. """ - def __init__(self, label: str, requires_permissions: bool = False): + def __init__( + self, label: str, requires_permissions: bool = False, optional: bool = False + ): """ Constructs an input component. """ self.set_interpret_parameters() + self.optional = optional super().__init__(label, requires_permissions) def preprocess(self, x: Any) -> Any: @@ -97,6 +100,12 @@ class InputComponent(Component): """ pass + def get_template_context(self): + return { + "optional": self.optional, + **super().get_template_context(), + } + class Textbox(InputComponent): """ @@ -248,16 +257,22 @@ class Number(InputComponent): Demos: tax_calculator, titanic_survival """ - def __init__(self, default: Optional[float] = None, label: Optional[str] = None): + def __init__( + self, + default: Optional[float] = None, + label: Optional[str] = None, + optional: bool = False, + ): """ Parameters: default (float): default value. label (str): component name in interface. + optional (bool): If True, the interface can be submitted with no value for this component. """ self.default = default self.test_input = default if default is not None else 1 self.interpret_by_tokens = False - super().__init__(label) + super().__init__(label, optional=optional) def get_template_context(self): return {"default": self.default, **super().get_template_context()} @@ -268,13 +283,15 @@ class Number(InputComponent): "number": {}, } - def preprocess(self, x: Number) -> float: + def preprocess(self, x: Optional[Number]) -> Optional[float]: """ Parameters: - x (number): numeric input + x (string): numeric input as a string Returns: (float): number representing function input """ + if self.optional and x is None: + return None return float(x) def preprocess_example(self, x: float) -> float: @@ -445,7 +462,7 @@ class Checkbox(InputComponent): "checkbox": {}, } - def preprocess(self, x): + def preprocess(self, x: bool) -> bool: """ Parameters: x (bool): boolean input @@ -519,7 +536,7 @@ class CheckboxGroup(InputComponent): **super().get_template_context(), } - def preprocess(self, x): + def preprocess(self, x: List[str]) -> List[str] | List[int]: """ Parameters: x (List[str]): list of selected choices @@ -590,7 +607,7 @@ class Radio(InputComponent): def __init__( self, - choices: List(str), + choices: List[str], type: str = "value", default: Optional[str] = None, label: Optional[str] = None, @@ -616,7 +633,7 @@ class Radio(InputComponent): **super().get_template_context(), } - def preprocess(self, x): + def preprocess(self, x: str) -> str | int: """ Parameters: x (str): selected choice @@ -692,7 +709,7 @@ class Dropdown(InputComponent): **super().get_template_context(), } - def preprocess(self, x): + def preprocess(self, x: str) -> str | int: """ Parameters: x (str): selected choice @@ -768,11 +785,10 @@ class Image(InputComponent): requires_permissions = source == "webcam" self.tool = tool self.type = type - self.optional = optional self.invert_colors = invert_colors self.test_input = test_data.BASE64_IMAGE self.interpret_by_tokens = True - super().__init__(label, requires_permissions) + super().__init__(label, requires_permissions, optional=optional) @classmethod def get_shortcut_implementations(cls): @@ -797,12 +813,12 @@ class Image(InputComponent): **super().get_template_context(), } - def preprocess(self, x): + def preprocess(self, x: Optional[str]) -> np.array | PIL.Image | str | None: """ Parameters: x (str): base64 url data Returns: - (Union[numpy.array, PIL.Image, file-object]): image in requested format + (Union[numpy.array, PIL.Image, filepath]): image in requested format """ if x is None: return x @@ -993,8 +1009,7 @@ class Video(InputComponent): """ self.type = type self.source = source - self.optional = optional - super().__init__(label) + super().__init__(label, optional=optional) @classmethod def get_shortcut_implementations(cls): @@ -1012,7 +1027,7 @@ class Video(InputComponent): def preprocess_example(self, x): return {"name": x, "data": None, "is_example": True} - def preprocess(self, x): + def preprocess(self, x: Dict[str, str] | None) -> str | None: """ Parameters: x (Dict[name: str, data: str]): JSON object with filename as 'name' property and base64 data as 'data' property @@ -1081,10 +1096,9 @@ class Audio(InputComponent): self.source = source requires_permissions = source == "microphone" self.type = type - self.optional = optional self.test_input = test_data.BASE64_AUDIO self.interpret_by_tokens = True - super().__init__(label, requires_permissions) + super().__init__(label, requires_permissions, optional=optional) def get_template_context(self): return { @@ -1104,12 +1118,12 @@ class Audio(InputComponent): def preprocess_example(self, x): return {"name": x, "data": None, "is_example": True} - def preprocess(self, x): + def preprocess(self, x: Dict[str, str] | None) -> Tuple[int, np.array] | str | None: """ Parameters: x (Dict[name: str, data: str]): JSON object with filename as 'name' property and base64 data as 'data' property Returns: - (Union[Tuple[int, numpy.array], file-object, numpy.array]): audio in requested format + (Union[Tuple[int, numpy.array], str, numpy.array]): audio in requested format """ if x is None: return x @@ -1295,8 +1309,7 @@ class File(InputComponent): self.file_count = file_count self.type = type self.test_input = None - self.optional = optional - super().__init__(label) + super().__init__(label, optional=optional) def get_template_context(self): return { @@ -1315,7 +1328,7 @@ class File(InputComponent): def preprocess_example(self, x): return {"name": x, "data": None, "is_example": True} - def preprocess(self, x): + def preprocess(self, x: List[Dict[str, str]] | None): """ Parameters: x (List[Dict[name: str, data: str]]): List of JSON objects with filename as 'name' property and base64 data as 'data' property @@ -1445,7 +1458,7 @@ class Dataframe(InputComponent): "list": {"type": "array", "col_count": 1}, } - def preprocess(self, x): + def preprocess(self, x: List[List[str | Number | bool]]): """ Parameters: x (List[List[Union[str, number, bool]]]): 2D array of str, numeric, or bool data @@ -1508,8 +1521,7 @@ class Timeseries(InputComponent): if isinstance(y, str): y = [y] self.y = y - self.optional = optional - super().__init__(label) + super().__init__(label, optional=optional) def get_template_context(self): return { @@ -1528,7 +1540,7 @@ class Timeseries(InputComponent): def preprocess_example(self, x): return {"name": x, "is_example": True} - def preprocess(self, x): + def preprocess(self, x: Dict | None) -> pd.DataFrame | None: """ Parameters: x (Dict[data: List[List[Union[str, number, bool]]], headers: List[str], range: List[number]]): Dict with keys 'data': 2D array of str, numeric, or bool data, 'headers': list of strings for header names, 'range': optional two element list designating start of end of subrange. diff --git a/gradio/templates/frontend/index.html b/gradio/templates/frontend/index.html index 436d6bf845..19b91a2ab7 100644 --- a/gradio/templates/frontend/index.html +++ b/gradio/templates/frontend/index.html @@ -45,10 +45,10 @@ Gradio - - + + - + diff --git a/scripts/format_backend.sh b/scripts/format_backend.sh index f08bb097f6..7858410428 100644 --- a/scripts/format_backend.sh +++ b/scripts/format_backend.sh @@ -3,7 +3,7 @@ if [ -z "$(ls | grep CONTRIBUTING.md)" ]; then echo "Please run the script from repo directory" exit -1 else - echo "Installing formatting with black and isort, also checking for standards with flake8" + echo "Formatting backend and tests with black and isort, also checking for standards with flake8" python -m black gradio test python -m isort --profile=black gradio test python -m flake8 --ignore=E731,E501,E722,W503,E126,F401,E203 gradio test diff --git a/scripts/format_frontend.sh b/scripts/format_frontend.sh new file mode 100644 index 0000000000..15517ee13b --- /dev/null +++ b/scripts/format_frontend.sh @@ -0,0 +1,10 @@ +#!/bin/bash +if [ -z "$(ls | grep CONTRIBUTING.md)" ]; then + echo "Please run the script from repo directory" + exit -1 +else + echo "Formatting frontend with prettier, also type checking with TypeScript" + cd ui + pnpm format:write + pnpm ts:check +fi diff --git a/test/test_inputs.py b/test/test_inputs.py index f6cb104216..83b8fe4a7c 100644 --- a/test/test_inputs.py +++ b/test/test_inputs.py @@ -123,8 +123,9 @@ class TestTextbox(unittest.TestCase): class TestNumber(unittest.TestCase): def test_as_component(self): - numeric_input = gr.inputs.Number() + numeric_input = gr.inputs.Number(optional=True) self.assertEqual(numeric_input.preprocess(3), 3.0) + self.assertEqual(numeric_input.preprocess(None), None) self.assertEqual(numeric_input.preprocess_example(3), 3) self.assertEqual(numeric_input.serialize(3, True), 3) with tempfile.TemporaryDirectory() as tmpdirname: @@ -143,6 +144,10 @@ class TestNumber(unittest.TestCase): numeric_input.get_interpretation_neighbors(1), ([0.97, 0.98, 0.99, 1.01, 1.02, 1.03], {}), ) + self.assertEqual( + numeric_input.get_template_context(), + {"default": None, "optional": True, "name": "number", "label": None}, + ) def test_in_interface(self): iface = gr.Interface(lambda x: x**2, "number", "textbox") @@ -204,6 +209,7 @@ class TestSlider(unittest.TestCase): "step": 1, "default": 15, "name": "slider", + "optional": False, "label": "Slide Your Input", }, ) @@ -262,7 +268,12 @@ class TestCheckbox(unittest.TestCase): bool_input = gr.inputs.Checkbox(default=True, label="Check Your Input") self.assertEqual( bool_input.get_template_context(), - {"default": True, "name": "checkbox", "label": "Check Your Input"}, + { + "default": True, + "name": "checkbox", + "optional": False, + "label": "Check Your Input", + }, ) def test_in_interface(self): @@ -301,6 +312,7 @@ class TestCheckboxGroup(unittest.TestCase): { "choices": ["a", "b", "c"], "default": ["a", "c"], + "optional": False, "name": "checkboxgroup", "label": "Check Your Inputs", }, @@ -349,6 +361,7 @@ class TestRadio(unittest.TestCase): "default": "a", "name": "radio", "label": "Pick Your One Input", + "optional": False, }, ) with self.assertRaises(ValueError): @@ -393,6 +406,7 @@ class TestDropdown(unittest.TestCase): "default": "a", "name": "dropdown", "label": "Drop Your Input", + "optional": False, }, ) with self.assertRaises(ValueError): @@ -651,6 +665,7 @@ class TestDataframe(unittest.TestCase): "default": [[None, None, None], [None, None, None], [None, None, None]], "name": "dataframe", "label": "Dataframe Input", + "optional": False, }, ) dataframe_input = gr.inputs.Dataframe() diff --git a/ui/packages/app/src/Interface.svelte b/ui/packages/app/src/Interface.svelte index 864daa0ab5..c8d5744064 100644 --- a/ui/packages/app/src/Interface.svelte +++ b/ui/packages/app/src/Interface.svelte @@ -200,7 +200,10 @@ {#each input_components as input_component, i} {#if input_component.name !== "state"}
-
{input_component.label}
+
+ {input_component.label}{#if input_component.optional} +  (optional){/if} +