diff --git a/src/python-fastui/fastui/json_schema.py b/src/python-fastui/fastui/json_schema.py index 49492796..53ee526d 100644 --- a/src/python-fastui/fastui/json_schema.py +++ b/src/python-fastui/fastui/json_schema.py @@ -210,7 +210,7 @@ def json_schema_array_to_fields( items_schema = schema.get('items') if items_schema: items_schema, required = deference_json_schema(items_schema, defs, required) - for field_name in 'search_url', 'placeholder': + for field_name in 'search_url', 'placeholder', 'description': if value := schema.get(field_name): items_schema[field_name] = value # type: ignore if field := special_string_field(items_schema, loc_to_name(loc), title, required, True): @@ -312,7 +312,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: # If anyOf is a single type and null, then it is optional diff --git a/src/python-fastui/tests/test_forms.py b/src/python-fastui/tests/test_forms.py index b0919fad..d73e3c10 100644 --- a/src/python-fastui/tests/test_forms.py +++ b/src/python-fastui/tests/test_forms.py @@ -1,3 +1,4 @@ +import enum from contextlib import asynccontextmanager from io import BytesIO from typing import List, Tuple, Union @@ -6,7 +7,7 @@ from fastapi import HTTPException from fastui import components from fastui.forms import FormFile, Textarea, fastui_form -from pydantic import BaseModel +from pydantic import BaseModel, Field from starlette.datastructures import FormData, Headers, UploadFile from typing_extensions import Annotated @@ -469,3 +470,55 @@ def test_form_textarea_form_fields(): } ], } + + +class SelectEnum(str, enum.Enum): + one = 'one' + two = 'two' + + +class FormSelectMultiple(BaseModel): + select_single: SelectEnum = Field(title='Select Single', description='first field') + select_single_2: SelectEnum = Field(title='Select Single') # unset description to test leakage from prev. field + select_multiple: List[SelectEnum] = Field(title='Select Multiple', description='third field') + + +def test_form_description_leakage(): + m = components.ModelForm(model=FormSelectMultiple, submit_url='/foobar/') + + assert m.model_dump(by_alias=True, exclude_none=True) == { + 'formFields': [ + { + 'description': 'first field', + 'locked': False, + 'multiple': False, + 'name': 'select_single', + 'options': [{'label': 'One', 'value': 'one'}, {'label': 'Two', 'value': 'two'}], + 'required': True, + 'title': ['Select Single'], + 'type': 'FormFieldSelect', + }, + { + 'locked': False, + 'multiple': False, + 'name': 'select_single_2', + 'options': [{'label': 'One', 'value': 'one'}, {'label': 'Two', 'value': 'two'}], + 'required': True, + 'title': ['Select Single'], + 'type': 'FormFieldSelect', + }, + { + 'description': 'third field', + 'locked': False, + 'multiple': True, + 'name': 'select_multiple', + 'options': [{'label': 'One', 'value': 'one'}, {'label': 'Two', 'value': 'two'}], + 'required': True, + 'title': ['Select Multiple'], + 'type': 'FormFieldSelect', + }, + ], + 'method': 'POST', + 'submitUrl': '/foobar/', + 'type': 'ModelForm', + }