diff --git a/demo/uploader.py b/demo/uploader.py index a3257b5eb..5a3492dbd 100644 --- a/demo/uploader.py +++ b/demo/uploader.py @@ -5,10 +5,7 @@ @me.stateclass class State: - name: str - size: int - mime_type: str - contents: str + file: me.UploadedFile @me.page( @@ -29,19 +26,22 @@ def app(): style=me.Style(font_weight="bold"), ) - if state.contents: + if state.file.size: with me.box(style=me.Style(margin=me.Margin.all(10))): - me.text(f"File name: {state.name}") - me.text(f"File size: {state.size}") - me.text(f"File type: {state.mime_type}") + me.text(f"File name: {state.file.name}") + me.text(f"File size: {state.file.size}") + me.text(f"File type: {state.file.mime_type}") with me.box(style=me.Style(margin=me.Margin.all(10))): - me.image(src=state.contents) + me.image(src=_convert_contents_data_url(state.file)) def handle_upload(event: me.UploadEvent): state = me.state(State) - state.name = event.file.name - state.size = event.file.size - state.mime_type = event.file.mime_type - state.contents = f"data:{event.file.mime_type};base64,{base64.b64encode(event.file.getvalue()).decode()}" + state.file = event.file + + +def _convert_contents_data_url(file: me.UploadedFile) -> str: + return ( + f"data:{file.mime_type};base64,{base64.b64encode(file.getvalue()).decode()}" + ) diff --git a/mesop/components/uploader/BUILD b/mesop/components/uploader/BUILD index da63c5888..dbccee4c8 100644 --- a/mesop/components/uploader/BUILD +++ b/mesop/components/uploader/BUILD @@ -8,6 +8,7 @@ package( mesop_component( name = "uploader", assets = [":uploader.css"], + py_deps = [":uploaded_file"], ) sass_binary( @@ -15,3 +16,8 @@ sass_binary( src = "uploader.scss", sourcemap = False, ) + +py_library( + name = "uploaded_file", + srcs = ["uploaded_file.py"], +) diff --git a/mesop/components/uploader/e2e/uploader_app.py b/mesop/components/uploader/e2e/uploader_app.py index 4591bacac..b83abb623 100644 --- a/mesop/components/uploader/e2e/uploader_app.py +++ b/mesop/components/uploader/e2e/uploader_app.py @@ -5,34 +5,38 @@ @me.stateclass class State: - name: str - size: int - mime_type: str - contents: str + file: me.UploadedFile @me.page(path="/components/uploader/e2e/uploader_app") def app(): state = me.state(State) - me.uploader( - label="Upload Image", - accepted_file_types=["image/jpeg", "image/png"], - on_upload=handle_upload, - ) - - if state.contents: - with me.box(style=me.Style(margin=me.Margin.all(10))): - me.text(f"File name: {state.name}") - me.text(f"File size: {state.size}") - me.text(f"File type: {state.mime_type}") - - with me.box(style=me.Style(margin=me.Margin.all(10))): - me.image(src=state.contents) + with me.box(style=me.Style(padding=me.Padding.all(15))): + me.uploader( + label="Upload Image", + accepted_file_types=["image/jpeg", "image/png"], + on_upload=handle_upload, + type="flat", + color="primary", + style=me.Style(font_weight="bold"), + ) + + if state.file.size: + with me.box(style=me.Style(margin=me.Margin.all(10))): + me.text(f"File name: {state.file.name}") + me.text(f"File size: {state.file.size}") + me.text(f"File type: {state.file.mime_type}") + + with me.box(style=me.Style(margin=me.Margin.all(10))): + me.image(src=_convert_contents_data_url(state.file)) def handle_upload(event: me.UploadEvent): state = me.state(State) - state.name = event.file.name - state.size = event.file.size - state.mime_type = event.file.mime_type - state.contents = f"data:{event.file.mime_type};base64,{base64.b64encode(event.file.getvalue()).decode()}" + state.file = event.file + + +def _convert_contents_data_url(file: me.UploadedFile) -> str: + return ( + f"data:{file.mime_type};base64,{base64.b64encode(file.getvalue()).decode()}" + ) diff --git a/mesop/components/uploader/e2e/uploader_test.ts b/mesop/components/uploader/e2e/uploader_test.ts index 0a898bbbf..263e01f4f 100644 --- a/mesop/components/uploader/e2e/uploader_test.ts +++ b/mesop/components/uploader/e2e/uploader_test.ts @@ -13,6 +13,8 @@ test('test upload file', async ({page}) => { await expect(page.getByText('File size: 30793')).toHaveCount(1); await expect(page.getByText('File type: image/jpeg')).toHaveCount(1); await expect( - page.locator(`//img[contains(@src, "data:image/jpeg;base64")]`), + page.locator( + `//img[@src="data:image/jpeg;base64,/9j/4QD+RXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAHAAAAcgITAAMAAAABAAEAAIdpAAQAAAABAAAAegAAAAAAAABIAAAAAQAAAEgAAAABUGljYXNhAAAACJAAAAcAAAAEMDIyMZADAAIAAAAUAAAA4JEBAAcAAAAEAQIDAKAAAAcAAAAEMDEwMKABAAMAAAABAAEAAKACAAQAAAABAAAA+qADAAQAAAABAAAA+qQGAAMAAAABAAAAAAAAAAAyMDI0OjAyOjIyIDA0OjIwOjIzAAAA/+EDxGh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpJcHRjNHhtcEV4dD0iaHR0cDovL2lwdGMub3JnL3N0ZC9JcHRjNHhtcEV4dC8yMDA4LTAyLTI5LyIKICAgICAgICAgICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8SXB0YzR4bXBFeHQ6RGlnaXRhbFNvdXJjZUZpbGVUeXBlPmh0dHA6Ly9jdi5pcHRjLm9yZy9uZXdzY29kZXMvZGlnaXRhbHNvdXJjZXR5cGUvY29tcG9zaXRlV2l0aFRyYWluZWRBbGdvcml0aG1pY01lZGlhPC9JcHRjNHhtcEV4dDpEaWdpdGFsU291cmNlRmlsZVR5cGU+CiAgICAgICAgIDxJcHRjNHhtcEV4dDpEaWdpdGFsU291cmNlVHlwZT5odHRwOi8vY3YuaXB0Yy5vcmcvbmV3c2NvZGVzL2RpZ2l0YWxzb3VyY2V0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"]`, + ), ).toHaveCount(1); }); diff --git a/mesop/components/uploader/uploaded_file.py b/mesop/components/uploader/uploaded_file.py new file mode 100644 index 000000000..371c4a989 --- /dev/null +++ b/mesop/components/uploader/uploaded_file.py @@ -0,0 +1,42 @@ +import io + + +# Store this class in a separate file so we can more easily reference +# in dataclass utils. +class UploadedFile(io.BytesIO): + """Uploaded file contents and metadata.""" + + def __init__( + self, + contents: bytes = b"", + *, + name: str = "", + size: int = 0, + mime_type: str = "", + ): + super().__init__(contents) + self._name = name + self._size = size + self._mime_type = mime_type + + @property + def name(self): + return self._name + + @property + def size(self): + return self._size + + @property + def mime_type(self): + return self._mime_type + + def __eq__(self, other): + if isinstance(other, UploadedFile): + return (self.getvalue(), self.name, self.size, self.mime_type) == ( + other.getvalue(), + other.name, + other.size, + other.mime_type, + ) + return False diff --git a/mesop/components/uploader/uploader.py b/mesop/components/uploader/uploader.py index 9b32ad696..bcc67e239 100644 --- a/mesop/components/uploader/uploader.py +++ b/mesop/components/uploader/uploader.py @@ -1,4 +1,3 @@ -import io from dataclasses import dataclass from typing import Any, Callable, Literal, Sequence @@ -10,32 +9,11 @@ register_event_mapper, register_native_component, ) +from mesop.components.uploader.uploaded_file import UploadedFile from mesop.events import MesopEvent from mesop.exceptions import MesopDeveloperException -class UploadedFile(io.BytesIO): - """Uploaded file contents and metadata.""" - - def __init__(self, contents: bytes, *, name: str, size: int, mime_type: str): - super().__init__(contents) - self._name = name - self._size = size - self._mime_type = mime_type - - @property - def name(self): - return self._name - - @property - def size(self): - return self._size - - @property - def mime_type(self): - return self._mime_type - - @dataclass(kw_only=True) class UploadEvent(MesopEvent): """Event for file uploads. diff --git a/mesop/dataclass_utils/BUILD b/mesop/dataclass_utils/BUILD index f6909e01d..f86151989 100644 --- a/mesop/dataclass_utils/BUILD +++ b/mesop/dataclass_utils/BUILD @@ -10,17 +10,26 @@ py_library( ["*.py"], exclude = ["*_test.py"], ), - deps = ["//mesop/exceptions"] + THIRD_PARTY_PY_DEEPDIFF, + deps = [ + "//mesop/components/uploader:uploaded_file", + "//mesop/exceptions", + ] + THIRD_PARTY_PY_DEEPDIFF, ) py_test( name = "dataclass_utils_test", srcs = ["dataclass_utils_test.py"], - deps = [":dataclass_utils"] + THIRD_PARTY_PY_PYTEST + THIRD_PARTY_PY_PANDAS, + deps = [ + ":dataclass_utils", + "//mesop/components/uploader:uploaded_file", + ] + THIRD_PARTY_PY_PYTEST + THIRD_PARTY_PY_PANDAS, ) py_test( name = "diff_state_test", srcs = ["diff_state_test.py"], - deps = [":dataclass_utils"] + THIRD_PARTY_PY_PYTEST + THIRD_PARTY_PY_PANDAS, + deps = [ + ":dataclass_utils", + "//mesop/components/uploader:uploaded_file", + ] + THIRD_PARTY_PY_PYTEST + THIRD_PARTY_PY_PANDAS, ) diff --git a/mesop/dataclass_utils/dataclass_utils.py b/mesop/dataclass_utils/dataclass_utils.py index 2fc142df1..acac68e10 100644 --- a/mesop/dataclass_utils/dataclass_utils.py +++ b/mesop/dataclass_utils/dataclass_utils.py @@ -10,12 +10,15 @@ from deepdiff.operator import BaseOperator from deepdiff.path import parse_path +from mesop.components.uploader.uploaded_file import UploadedFile from mesop.exceptions import MesopDeveloperException, MesopException _PANDAS_OBJECT_KEY = "__pandas.DataFrame__" _DATETIME_OBJECT_KEY = "__datetime.datetime__" _BYTES_OBJECT_KEY = "__python.bytes__" +_UPLOADED_FILE_OBJECT_KEY = "__mesop.UploadedFile__" _DIFF_ACTION_DATA_FRAME_CHANGED = "data_frame_changed" +_DIFF_ACTION_UPLOADED_FILE_CHANGED = "mesop_uploaded_file_changed" C = TypeVar("C") @@ -174,8 +177,19 @@ def default(self, obj): except ImportError: pass + if isinstance(obj, UploadedFile): + return { + _UPLOADED_FILE_OBJECT_KEY: { + "contents": base64.b64encode(obj.getvalue()).decode("utf-8"), + "name": obj.name, + "size": obj.size, + "mime_type": obj.mime_type, + } + } + if isinstance(obj, datetime): return {_DATETIME_OBJECT_KEY: obj.isoformat()} + if isinstance(obj, bytes): return {_BYTES_OBJECT_KEY: base64.b64encode(obj).decode("utf-8")} @@ -210,6 +224,14 @@ def decode_mesop_json_state_hook(dct): if _BYTES_OBJECT_KEY in dct: return base64.b64decode(dct[_BYTES_OBJECT_KEY]) + if _UPLOADED_FILE_OBJECT_KEY in dct: + return UploadedFile( + base64.b64decode(dct[_UPLOADED_FILE_OBJECT_KEY]["contents"]), + name=dct[_UPLOADED_FILE_OBJECT_KEY]["name"], + size=dct[_UPLOADED_FILE_OBJECT_KEY]["size"], + mime_type=dct[_UPLOADED_FILE_OBJECT_KEY]["mime_type"], + ) + return dct @@ -241,6 +263,29 @@ def give_up_diffing(self, level, diff_instance) -> bool: return True +class UploadedFileOperator(BaseOperator): + """Custom operator to detect changes in UploadedFile class. + + DeepDiff does not diff the UploadedFile class correctly, so we will just use a normal + equality check, rather than diffing further into the io.BytesIO parent class. + + This class could probably be made more generic to handle other classes where we want + to diff using equality checks. + """ + + def match(self, level) -> bool: + return isinstance(level.t1, UploadedFile) and isinstance( + level.t2, UploadedFile + ) + + def give_up_diffing(self, level, diff_instance) -> bool: + if level.t1 != level.t2: + diff_instance.custom_report_result( + _DIFF_ACTION_UPLOADED_FILE_CHANGED, level, {"value": level.t2} + ) + return True + + def diff_state(state1: Any, state2: Any) -> str: """ Diffs two state objects and returns the difference using DeepDiff's Delta format as a @@ -255,11 +300,13 @@ def diff_state(state1: Any, state2: Any) -> str: raise MesopException("Tried to diff state which was not a dataclass") custom_actions = [] - + custom_operators = [UploadedFileOperator()] # Only use the `DataFrameOperator` if pandas exists. if _has_pandas: differences = DeepDiff( - state1, state2, custom_operators=[DataFrameOperator()] + state1, + state2, + custom_operators=[*custom_operators, DataFrameOperator()], ) # Manually format dataframe diffs to flat dict format. @@ -273,7 +320,18 @@ def diff_state(state1: Any, state2: Any) -> str: for path, diff in differences[_DIFF_ACTION_DATA_FRAME_CHANGED].items() ] else: - differences = DeepDiff(state1, state2) + differences = DeepDiff(state1, state2, custom_operators=custom_operators) + + # Manually format UploadedFile diffs to flat dict format. + if _DIFF_ACTION_UPLOADED_FILE_CHANGED in differences: + custom_actions = [ + { + "path": parse_path(path), + "action": _DIFF_ACTION_UPLOADED_FILE_CHANGED, + **diff, + } + for path, diff in differences[_DIFF_ACTION_UPLOADED_FILE_CHANGED].items() + ] return json.dumps( Delta(differences, always_include_values=True).to_flat_dicts() diff --git a/mesop/dataclass_utils/dataclass_utils_test.py b/mesop/dataclass_utils/dataclass_utils_test.py index 3b48cbd48..7ebf5257a 100644 --- a/mesop/dataclass_utils/dataclass_utils_test.py +++ b/mesop/dataclass_utils/dataclass_utils_test.py @@ -6,6 +6,7 @@ import pytest import mesop.protos.ui_pb2 as pb +from mesop.components.uploader.uploaded_file import UploadedFile from mesop.dataclass_utils.dataclass_utils import ( dataclass_with_defaults, has_parent, @@ -43,6 +44,11 @@ class WithBytes: data: bytes = b"" +@dataclass +class WithUploadedFile: + data: UploadedFile = field(default_factory=UploadedFile) + + JSON_STR = """{"b": {"c": {"val": ""}}, "list_b": [ {"c": {"val": "1"}}, @@ -159,6 +165,21 @@ def test_serialize_pandas_dataframe(): ) +def test_serialize_uploaded_file(): + serialized_dataclass = serialize_dataclass( + WithUploadedFile( + data=UploadedFile( + b"data", name="file.png", size=10, mime_type="image/png" + ) + ) + ) + + assert ( + serialized_dataclass + == '{"data": {"__mesop.UploadedFile__": {"contents": "ZGF0YQ==", "name": "file.png", "size": 10, "mime_type": "image/png"}}}' + ) + + @pytest.mark.parametrize( "input_bytes, expected_json", [ @@ -238,6 +259,18 @@ def test_update_dataclass_with_pandas_dataframe(): ) +def test_update_dataclass_with_uploaded_file(): + uploaded_file = UploadedFile( + b"data", name="file.png", size=10, mime_type="image/png" + ) + serialized_dataclass = serialize_dataclass( + WithUploadedFile(data=uploaded_file) + ) + uploaded_file_state = WithUploadedFile() + update_dataclass_from_json(uploaded_file_state, serialized_dataclass) + assert uploaded_file_state.data == uploaded_file + + def test_update_dataclass_with_bytes(): bytes_data = b"hello world" serialized_dataclass = serialize_dataclass(WithBytes(data=bytes_data)) diff --git a/mesop/dataclass_utils/diff_state_test.py b/mesop/dataclass_utils/diff_state_test.py index 39e918cc8..fd284e5f8 100644 --- a/mesop/dataclass_utils/diff_state_test.py +++ b/mesop/dataclass_utils/diff_state_test.py @@ -6,6 +6,7 @@ import pandas as pd import pytest +from mesop.components.uploader.uploaded_file import UploadedFile from mesop.dataclass_utils.dataclass_utils import diff_state from mesop.exceptions import MesopException @@ -395,6 +396,47 @@ class C: ] +def test_diff_uploaded_file(): + @dataclass + class C: + data: UploadedFile + + s1 = C(data=UploadedFile()) + s2 = C( + data=UploadedFile(b"data", name="file.png", size=10, mime_type="image/png") + ) + + assert json.loads(diff_state(s1, s2)) == [ + { + "path": ["data"], + "action": "mesop_uploaded_file_changed", + "value": { + "__mesop.UploadedFile__": { + "contents": "ZGF0YQ==", + "name": "file.png", + "size": 10, + "mime_type": "image/png", + }, + }, + } + ] + + +def test_diff_uploaded_file_same_no_diff(): + @dataclass + class C: + data: UploadedFile + + s1 = C( + data=UploadedFile(b"data", name="file.png", size=10, mime_type="image/png") + ) + s2 = C( + data=UploadedFile(b"data", name="file.png", size=10, mime_type="image/png") + ) + + assert json.loads(diff_state(s1, s2)) == [] + + # The diff will pass, but the Mesop JSON serializer currently fails on sets in state class. # See https://github.com/google/mesop/issues/387 def test_diff_set(): diff --git a/mesop/web/src/utils/diff.ts b/mesop/web/src/utils/diff.ts index 33f366a5f..bb763261b 100644 --- a/mesop/web/src/utils/diff.ts +++ b/mesop/web/src/utils/diff.ts @@ -78,6 +78,7 @@ export function applyComponentDiff(component: Component, diff: ComponentDiff) { const STATE_DIFF_VALUES_CHANGED = 'values_changed'; const STATE_DIFF_TYPE_CHANGES = 'type_changes'; const STATE_DIFF_DATA_FRAME_CHANGED = 'data_frame_changed'; +const STATE_DIFF_UPLOADED_FILE_CHANGED = 'mesop_uploaded_file_changed'; const STATE_DIFF_ITERABLE_ITEM_REMOVED = 'iterable_item_removed'; const STATE_DIFF_ITERABLE_ITEM_ADDED = 'iterable_item_added'; const STATE_DIFF_DICT_ITEM_REMOVED = 'dictionary_item_removed'; @@ -114,7 +115,8 @@ export function applyStateDiff(stateJson: string, diffJson: string): string { if ( row.action === STATE_DIFF_VALUES_CHANGED || row.action === STATE_DIFF_TYPE_CHANGES || - row.action === STATE_DIFF_DATA_FRAME_CHANGED + row.action === STATE_DIFF_DATA_FRAME_CHANGED || + row.action === STATE_DIFF_UPLOADED_FILE_CHANGED ) { updateValue(root, row.path, row.value); } else if (row.action === STATE_DIFF_DICT_ITEM_ADDED) { diff --git a/mesop/web/src/utils/diff_state_spec.ts b/mesop/web/src/utils/diff_state_spec.ts index ad960190d..52c0d8ced 100644 --- a/mesop/web/src/utils/diff_state_spec.ts +++ b/mesop/web/src/utils/diff_state_spec.ts @@ -373,4 +373,44 @@ describe('applyStateDiff functionality', () => { }), ); }); + + it('applies UploadedFile updates', () => { + const state1 = JSON.stringify({ + data: { + '__mesop.UploadedFile__': { + 'contents': '', + 'name': '', + 'size': 0, + 'mime_type': '', + }, + }, + }); + const diff = JSON.stringify([ + { + path: ['data'], + action: 'mesop_uploaded_file_changed', + value: { + '__mesop.UploadedFile__': { + 'contents': 'data', + 'name': 'file.png', + 'size': 10, + 'mime_type': 'image/png', + }, + }, + }, + ]); + + expect(applyStateDiff(state1, diff)).toBe( + JSON.stringify({ + data: { + '__mesop.UploadedFile__': { + 'contents': 'data', + 'name': 'file.png', + 'size': 10, + 'mime_type': 'image/png', + }, + }, + }), + ); + }); });