From 24faaed2e8cd5d4b9441f0dfcb59e363b20a6268 Mon Sep 17 00:00:00 2001 From: Ruben Date: Mon, 29 Jan 2024 15:12:39 -0800 Subject: [PATCH 1/6] feat(FormFields): accept className --- src/python-fastui/fastui/json_schema.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/python-fastui/fastui/json_schema.py b/src/python-fastui/fastui/json_schema.py index c822f180..0e59e651 100644 --- a/src/python-fastui/fastui/json_schema.py +++ b/src/python-fastui/fastui/json_schema.py @@ -185,6 +185,7 @@ def json_schema_field_to_field( initial=schema.get('default'), description=schema.get('description'), mode=schema.get('mode', 'checkbox'), + class_name=schema.get('className'), ) elif field := special_string_field(schema, name, title, required, False): return field @@ -198,6 +199,7 @@ def json_schema_field_to_field( autocomplete=schema.get('autocomplete'), description=schema.get('description'), placeholder=schema.get('placeholder'), + class_name=schema.get('className'), ) @@ -247,6 +249,7 @@ def special_string_field( multiple=multiple, accept=schema.get('accept'), description=schema.get('description'), + class_name=schema.get('className'), ) elif schema.get('format') == 'textarea': return FormFieldTextarea( @@ -271,6 +274,7 @@ def special_string_field( options=[SelectOption(value=v, label=enum_labels.get(v) or as_title(v)) for v in enum], initial=schema.get('default'), description=schema.get('description'), + class_name=schema.get('className'), autocomplete=schema.get('autocomplete'), ) elif search_url := schema.get('search_url'): @@ -283,6 +287,7 @@ def special_string_field( multiple=multiple, initial=schema.get('initial'), description=schema.get('description'), + class_name=schema.get('className'), ) From 7717bbd9cc727f6c5cde319de7a2d8655e0e287e Mon Sep 17 00:00:00 2001 From: Ruben Date: Wed, 28 Feb 2024 11:59:31 -0800 Subject: [PATCH 2/6] feat: add support for step --- src/npm-fastui/src/components/FormField.tsx | 3 +- src/npm-fastui/src/models.d.ts | 1 + src/python-fastui/fastui/components/forms.py | 3 ++ src/python-fastui/fastui/json_schema.py | 13 +++++++- src/python-fastui/tests/test_forms.py | 34 ++++++++++++++++++++ 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/npm-fastui/src/components/FormField.tsx b/src/npm-fastui/src/components/FormField.tsx index 23614570..3aeec849 100644 --- a/src/npm-fastui/src/components/FormField.tsx +++ b/src/npm-fastui/src/components/FormField.tsx @@ -24,7 +24,7 @@ interface FormFieldInputProps extends FormFieldInput { } export const FormFieldInputComp: FC = (props) => { - const { name, placeholder, required, htmlType, locked, autocomplete, onChange } = props + const { name, placeholder, required, htmlType, locked, autocomplete, onChange, step } = props return (
@@ -39,6 +39,7 @@ export const FormFieldInputComp: FC = (props) => { disabled={locked} placeholder={placeholder} autoComplete={autocomplete} + step={step} aria-describedby={descId(props)} onChange={onChange} /> diff --git a/src/npm-fastui/src/models.d.ts b/src/npm-fastui/src/models.d.ts index 258254de..100af916 100644 --- a/src/npm-fastui/src/models.d.ts +++ b/src/npm-fastui/src/models.d.ts @@ -433,6 +433,7 @@ export interface FormFieldInput { initial?: string | number placeholder?: string autocomplete?: string + step?: number | 'any' type: 'FormFieldInput' } /** diff --git a/src/python-fastui/fastui/components/forms.py b/src/python-fastui/fastui/components/forms.py index b4f2f2d9..92d8f386 100644 --- a/src/python-fastui/fastui/components/forms.py +++ b/src/python-fastui/fastui/components/forms.py @@ -58,6 +58,9 @@ class FormFieldInput(BaseFormField): autocomplete: _t.Union[str, None] = None """Autocomplete value for the field.""" + step: _t.Union[float, _t.Literal['any'], None] = None + """Step value for the field.""" + type: _t.Literal['FormFieldInput'] = 'FormFieldInput' """The type of the component. Always 'FormFieldInput'.""" diff --git a/src/python-fastui/fastui/json_schema.py b/src/python-fastui/fastui/json_schema.py index 0e59e651..83123a85 100644 --- a/src/python-fastui/fastui/json_schema.py +++ b/src/python-fastui/fastui/json_schema.py @@ -198,6 +198,7 @@ def json_schema_field_to_field( initial=schema.get('default'), autocomplete=schema.get('autocomplete'), description=schema.get('description'), + step=schema.get('step', get_default_step(schema)), placeholder=schema.get('placeholder'), class_name=schema.get('className'), ) @@ -262,6 +263,7 @@ def special_string_field( initial=schema.get('initial'), description=schema.get('description'), autocomplete=schema.get('autocomplete'), + class_name=schema.get('className'), ) elif enum := schema.get('enum'): enum_labels = schema.get('enum_labels', {}) @@ -274,8 +276,8 @@ def special_string_field( options=[SelectOption(value=v, label=enum_labels.get(v) or as_title(v)) for v in enum], initial=schema.get('default'), description=schema.get('description'), - class_name=schema.get('className'), autocomplete=schema.get('autocomplete'), + class_name=schema.get('className'), ) elif search_url := schema.get('search_url'): return FormFieldSelectSearch( @@ -318,6 +320,7 @@ def deference_json_schema( if def_schema is None: raise ValueError(f'Invalid $ref "{ref}", not found in {defs}') else: + return def_schema, required return def_schema.copy(), required # clone dict to avoid attribute leakage via shared schema. elif any_of := schema.get('anyOf'): if len(any_of) == 2 and sum(s.get('type') == 'null' for s in any_of) == 1: @@ -378,6 +381,14 @@ def input_html_type(schema: JsonSchemaField) -> InputHtmlType: raise ValueError(f'Unknown schema: {schema}') from e +def get_default_step(schema: JsonSchemaField) -> _t.Literal['any'] | None: + key = schema['type'] + if key == 'integer': + return None + if key == 'number': + return 'any' + + def schema_is_field(schema: JsonSchemaConcrete) -> _ta.TypeGuard[JsonSchemaField]: """ Determine if a schema is a field `JsonSchemaField` diff --git a/src/python-fastui/tests/test_forms.py b/src/python-fastui/tests/test_forms.py index ceaa7a5d..e2f52b11 100644 --- a/src/python-fastui/tests/test_forms.py +++ b/src/python-fastui/tests/test_forms.py @@ -547,3 +547,37 @@ def test_form_fields(): 'submitUrl': '/foobar/', 'type': 'ModelForm', } + + +class FormNumbersDefaultStep(BaseModel): + size: int + cost: float + + +def test_form_numbers_default_step(): + m = components.ModelForm(model=FormNumbersDefaultStep, submit_url='/foobar') + + assert m.model_dump(by_alias=True, exclude_none=True) == { + 'submitUrl': '/foobar', + 'method': 'POST', + 'type': 'ModelForm', + 'formFields': [ + { + 'name': 'size', + 'title': ['Size'], + 'required': True, + 'locked': False, + 'htmlType': 'number', + 'type': 'FormFieldInput', + }, + { + 'name': 'cost', + 'title': ['Cost'], + 'required': True, + 'locked': False, + 'htmlType': 'number', + 'step': 'any', + 'type': 'FormFieldInput', + }, + ], + } From df3dccf52cc22ab378c7f62823e9c7270b96f284 Mon Sep 17 00:00:00 2001 From: Ruben Gimenez Date: Wed, 28 Feb 2024 12:18:05 -0800 Subject: [PATCH 3/6] support older versions of python --- src/python-fastui/fastui/json_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python-fastui/fastui/json_schema.py b/src/python-fastui/fastui/json_schema.py index 83123a85..a8f6ad23 100644 --- a/src/python-fastui/fastui/json_schema.py +++ b/src/python-fastui/fastui/json_schema.py @@ -381,7 +381,7 @@ def input_html_type(schema: JsonSchemaField) -> InputHtmlType: raise ValueError(f'Unknown schema: {schema}') from e -def get_default_step(schema: JsonSchemaField) -> _t.Literal['any'] | None: +def get_default_step(schema: JsonSchemaField) -> _t.Union[_t.Literal['any'], None]: key = schema['type'] if key == 'integer': return None From b2a1f3276b061bafd441c74ca88155c82dbb74da Mon Sep 17 00:00:00 2001 From: Ruben Date: Mon, 4 Mar 2024 13:28:51 -0800 Subject: [PATCH 4/6] fix: form field Enum overrides --- src/python-fastui/fastui/json_schema.py | 1 + src/python-fastui/tests/test_forms.py | 57 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/python-fastui/fastui/json_schema.py b/src/python-fastui/fastui/json_schema.py index a8f6ad23..94a5d5c6 100644 --- a/src/python-fastui/fastui/json_schema.py +++ b/src/python-fastui/fastui/json_schema.py @@ -320,6 +320,7 @@ def deference_json_schema( if def_schema is None: raise ValueError(f'Invalid $ref "{ref}", not found in {defs}') else: + def_schema.update({k: v for k, v in schema.items() if k != '$ref'}) # type: ignore return def_schema, required return def_schema.copy(), required # clone dict to avoid attribute leakage via shared schema. elif any_of := schema.get('anyOf'): diff --git a/src/python-fastui/tests/test_forms.py b/src/python-fastui/tests/test_forms.py index e2f52b11..d22edb7c 100644 --- a/src/python-fastui/tests/test_forms.py +++ b/src/python-fastui/tests/test_forms.py @@ -1,5 +1,6 @@ import enum from contextlib import asynccontextmanager +from enum import Enum from io import BytesIO from typing import List, Tuple, Union @@ -581,3 +582,59 @@ def test_form_numbers_default_step(): }, ], } + + +class ToolEnum(str, Enum): + """Tools that can be leveraged to complete a job.""" + + hammer = 'hammer' + screwdriver = 'screwdriver' + saw = 'saw' + claw_hammer = 'claw_hammer' + + +class FormEnumWithOptionalEnum(BaseModel): + job_name: str + tool_required: ToolEnum | None = Field( + json_schema_extra={ + # Override certain schema fields. + 'placeholder': 'tool', + 'description': '', + } + ) + + +def test_form_with_enum(): + m = components.ModelForm(model=FormEnumWithOptionalEnum, submit_url='/foobar') + + assert m.model_dump(by_alias=True, exclude_none=True) == { + 'submitUrl': '/foobar', + 'method': 'POST', + 'type': 'ModelForm', + 'formFields': [ + { + 'name': 'job_name', + 'title': ['Job Name'], + 'required': True, + 'locked': False, + 'htmlType': 'text', + 'type': 'FormFieldInput', + }, + { + 'name': 'tool_required', + 'title': ['ToolEnum'], + 'required': False, + 'locked': False, + 'description': '', + 'options': [ + {'value': 'hammer', 'label': 'Hammer'}, + {'value': 'screwdriver', 'label': 'Screwdriver'}, + {'value': 'saw', 'label': 'Saw'}, + {'value': 'claw_hammer', 'label': 'Claw Hammer'}, + ], + 'multiple': False, + 'placeholder': 'tool', + 'type': 'FormFieldSelect', + }, + ], + } From e4996e57f2331ab19cb928241c337cfca7907648 Mon Sep 17 00:00:00 2001 From: Ruben Gimenez Date: Mon, 4 Mar 2024 13:43:31 -0800 Subject: [PATCH 5/6] tweak: python < 3.10 annotations --- src/python-fastui/tests/test_forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python-fastui/tests/test_forms.py b/src/python-fastui/tests/test_forms.py index d22edb7c..d0908a0b 100644 --- a/src/python-fastui/tests/test_forms.py +++ b/src/python-fastui/tests/test_forms.py @@ -595,7 +595,7 @@ class ToolEnum(str, Enum): class FormEnumWithOptionalEnum(BaseModel): job_name: str - tool_required: ToolEnum | None = Field( + tool_required: Union[ToolEnum, None] = Field( json_schema_extra={ # Override certain schema fields. 'placeholder': 'tool', From 9325ab9d54fc99a43b4a0811e12951d7d2edb045 Mon Sep 17 00:00:00 2001 From: Ruben Date: Mon, 10 Jun 2024 13:22:04 -0700 Subject: [PATCH 6/6] Sync fork with upstream --- src/python-fastui/fastui/json_schema.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/python-fastui/fastui/json_schema.py b/src/python-fastui/fastui/json_schema.py index 94a5d5c6..764817c9 100644 --- a/src/python-fastui/fastui/json_schema.py +++ b/src/python-fastui/fastui/json_schema.py @@ -320,9 +320,9 @@ def deference_json_schema( if def_schema is None: raise ValueError(f'Invalid $ref "{ref}", not found in {defs}') else: - def_schema.update({k: v for k, v in schema.items() if k != '$ref'}) # type: ignore - return def_schema, required - return def_schema.copy(), required # clone dict to avoid attribute leakage via shared schema. + def_schema_new = def_schema.copy() # clone dict to avoid attribute leakage via shared schema. + def_schema_new.update({k: v for k, v in schema.items() if k != '$ref'}) # type: ignore + return def_schema_new, required elif any_of := schema.get('anyOf'): if len(any_of) == 2 and sum(s.get('type') == 'null' for s in any_of) == 1: # If anyOf is a single type and null, then it is optional