diff --git a/src/python-fastui/fastui/forms.py b/src/python-fastui/fastui/forms.py index 759bf7c5..70a4ef03 100644 --- a/src/python-fastui/fastui/forms.py +++ b/src/python-fastui/fastui/forms.py @@ -1,5 +1,6 @@ import json import typing as _t +from contextlib import asynccontextmanager from itertools import groupby from mimetypes import MimeTypes from operator import itemgetter @@ -34,17 +35,18 @@ def __class_getitem__(cls, model: _t.Type[FormModel]) -> fastapi_params.Depends: def fastui_form(model: _t.Type[FormModel]) -> fastapi_params.Depends: + @asynccontextmanager async def run_fastui_form(request: fastapi.Request): async with request.form() as form_data: model_data = unflatten(form_data) - try: - return model.model_validate(model_data) - except pydantic.ValidationError as e: - raise fastapi.HTTPException( - status_code=422, - detail={'form': e.errors(include_input=False, include_url=False, include_context=False)}, - ) + try: + yield model.model_validate(model_data) + except pydantic.ValidationError as e: + raise fastapi.HTTPException( + status_code=422, + detail={'form': e.errors(include_input=False, include_url=False, include_context=False)}, + ) return fastapi.Depends(run_fastui_form) diff --git a/src/python-fastui/tests/test_forms.py b/src/python-fastui/tests/test_forms.py index b0919fad..f0257f2f 100644 --- a/src/python-fastui/tests/test_forms.py +++ b/src/python-fastui/tests/test_forms.py @@ -27,6 +27,9 @@ def __init__(self, form_data_list: List[Tuple[str, Union[str, UploadFile]]]): @asynccontextmanager async def form(self): yield self._form_data + for value in self._form_data.values(): + if isinstance(value, UploadFile): + value.file.close() def test_simple_form_fields(): @@ -94,9 +97,9 @@ async def test_simple_form_submit(): request = FakeRequest([('name', 'bar'), ('size', '123')]) - m = await form_dep.dependency(request) - assert isinstance(m, SimpleForm) - assert m.model_dump() == {'name': 'bar', 'size': 123} + async with form_dep.dependency(request) as m: + assert isinstance(m, SimpleForm) + assert m.model_dump() == {'name': 'bar', 'size': 123} async def test_simple_form_submit_repeat(): @@ -105,7 +108,8 @@ async def test_simple_form_submit_repeat(): request = FakeRequest([('name', 'bar'), ('size', '123'), ('size', '456')]) with pytest.raises(HTTPException) as exc_info: - await form_dep.dependency(request) + async with form_dep.dependency(request): + pass # insert_assert(exc_info.value.detail) assert exc_info.value.detail == { @@ -156,9 +160,9 @@ async def test_w_nested_form_submit(): request = FakeRequest([('name', 'bar'), ('nested.x', '123')]) - m = await form_dep.dependency(request) - assert isinstance(m, FormWithNested) - assert m.model_dump() == {'name': 'bar', 'nested': {'x': 123}} + async with form_dep.dependency(request) as m: + assert isinstance(m, FormWithNested) + assert m.model_dump() == {'name': 'bar', 'nested': {'x': 123}} class FormWithFile(BaseModel): @@ -190,8 +194,9 @@ async def test_file_submit(): file = UploadFile(BytesIO(b'foobar'), size=6, filename='testing.txt') request = FakeRequest([('profile_pic', file)]) - m = await fastui_form(FormWithFile).dependency(request) - assert m.model_dump() == {'profile_pic': file} + async with fastui_form(FormWithFile).dependency(request) as m: + assert m.model_dump() == {'profile_pic': file} + assert not m.profile_pic.file.closed async def test_file_submit_repeat(): @@ -200,7 +205,8 @@ async def test_file_submit_repeat(): request = FakeRequest([('profile_pic', file1), ('profile_pic', file2)]) with pytest.raises(HTTPException) as exc_info: - await fastui_form(FormWithFile).dependency(request) + async with fastui_form(FormWithFile).dependency(request): + pass # insert_assert(exc_info.value.detail) assert exc_info.value.detail == { @@ -239,16 +245,18 @@ async def test_file_constrained_submit(): file = UploadFile(BytesIO(b'foobar'), size=16_000, headers=headers) request = FakeRequest([('profile_pic', file)]) - m = await fastui_form(FormWithFileConstraint).dependency(request) - assert m.model_dump() == {'profile_pic': file} + async with fastui_form(FormWithFileConstraint).dependency(request) as m: + assert m.model_dump() == {'profile_pic': file} + assert not m.profile_pic.file.closed async def test_file_constrained_submit_filename(): file = UploadFile(BytesIO(b'foobar'), size=16_000, filename='image.png') request = FakeRequest([('profile_pic', file)]) - m = await fastui_form(FormWithFileConstraint).dependency(request) - assert m.model_dump() == {'profile_pic': file} + async with fastui_form(FormWithFileConstraint).dependency(request) as m: + assert m.model_dump() == {'profile_pic': file} + assert not m.profile_pic.file.closed async def test_file_constrained_submit_too_big(): @@ -257,7 +265,8 @@ async def test_file_constrained_submit_too_big(): request = FakeRequest([('profile_pic', file)]) with pytest.raises(HTTPException) as exc_info: - await fastui_form(FormWithFileConstraint).dependency(request) + async with fastui_form(FormWithFileConstraint).dependency(request): + pass # insert_assert(exc_info.value.detail) assert exc_info.value.detail == { @@ -277,7 +286,8 @@ async def test_file_constrained_submit_wrong_type(): request = FakeRequest([('profile_pic', file)]) with pytest.raises(HTTPException) as exc_info: - await fastui_form(FormWithFileConstraint).dependency(request) + async with fastui_form(FormWithFileConstraint).dependency(request): + pass # insert_assert(exc_info.value.detail) assert exc_info.value.detail == { @@ -323,8 +333,9 @@ async def test_multiple_files_single(): file = UploadFile(BytesIO(b'foobar'), size=16_000, filename='image.png') request = FakeRequest([('files', file)]) - m = await fastui_form(FormMultipleFiles).dependency(request) - assert m.model_dump() == {'files': [file]} + async with fastui_form(FormMultipleFiles).dependency(request) as m: + assert m.model_dump() == {'files': [file]} + assert not m.files[0].file.closed async def test_multiple_files_multiple(): @@ -332,8 +343,10 @@ async def test_multiple_files_multiple(): file2 = UploadFile(BytesIO(b'foobar'), size=6, filename='image2.png') request = FakeRequest([('files', file1), ('files', file2)]) - m = await fastui_form(FormMultipleFiles).dependency(request) - assert m.model_dump() == {'files': [file1, file2]} + async with fastui_form(FormMultipleFiles).dependency(request) as m: + assert m.model_dump() == {'files': [file1, file2]} + assert not m.files[0].file.closed + assert not m.files[1].file.closed class FixedTuple(BaseModel): @@ -379,8 +392,8 @@ def test_fixed_tuple(): async def test_fixed_tuple_submit(): request = FakeRequest([('foo.0', 'bar'), ('foo.1', '123'), ('foo.2', '456')]) - m = await fastui_form(FixedTuple).dependency(request) - assert m.model_dump() == {'foo': ('bar', 123, 456)} + async with fastui_form(FixedTuple).dependency(request) as m: + assert m.model_dump() == {'foo': ('bar', 123, 456)} class NestedTuple(BaseModel): @@ -426,8 +439,8 @@ def test_fixed_tuple_nested(): async def test_fixed_tuple_nested_submit(): request = FakeRequest([('bar.foo.0', 'bar'), ('bar.foo.1', '123'), ('bar.foo.2', '456')]) - m = await fastui_form(NestedTuple).dependency(request) - assert m.model_dump() == {'bar': {'foo': ('bar', 123, 456)}} + async with fastui_form(NestedTuple).dependency(request) as m: + assert m.model_dump() == {'bar': {'foo': ('bar', 123, 456)}} def test_variable_tuple():