Explorar el Código

Yield with uploads (#1339)

Elijah Ahianyo hace 1 año
padre
commit
ca11b82432
Se han modificado 3 ficheros con 53 adiciones y 36 borrados
  1. 20 15
      reflex/.templates/web/utils/state.js
  2. 16 12
      reflex/app.py
  3. 17 9
      tests/test_app.py

+ 20 - 15
reflex/.templates/web/utils/state.js

@@ -323,21 +323,26 @@ export const uploadFiles = async (state, setResult, handler) => {
   }
 
   // Send the file to the server.
-  await axios.post(UPLOADURL, formdata, headers).then((response) => {
-    // Apply the delta and set the result.
-    const update = response.data;
-    applyDelta(state, update.delta);
-
-    // Set processing to false and return.
-    setResult({
-      state: state,
-      events: update.events,
-      final: true,
-      processing: false,
-    });
-  });
-
-  return true;
+  await axios.post(UPLOADURL, formdata, headers)
+    .then(() => { return true; })
+    .catch(
+      error => {
+        if (error.response) {
+          // The request was made and the server responded with a status code
+          // that falls out of the range of 2xx
+          console.log(error.response.data);
+        } else if (error.request) {
+          // The request was made but no response was received
+          // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
+          // http.ClientRequest in node.js
+          console.log(error.request);
+        } else {
+          // Something happened in setting up the request that triggered an Error
+          console.log(error.message);
+        }
+        return false;
+      }
+    )
 };
 
 /**

+ 16 - 12
reflex/app.py

@@ -88,6 +88,9 @@ class App(Base):
     # The component to render if there is a connection error to the server.
     connect_error_component: Optional[Component] = None
 
+    # The async server name space
+    event_namespace: Optional[AsyncNamespace] = None
+
     def __init__(self, *args, **kwargs):
         """Initialize the app.
 
@@ -127,11 +130,10 @@ class App(Base):
         self.socket_app = ASGIApp(self.sio, socketio_path="")
 
         # Create the event namespace and attach the main app. Not related to any paths.
-        event_namespace = EventNamespace("/event", self)
+        self.event_namespace = EventNamespace("/event", self)
 
         # Register the event namespace with the socket.
-        self.sio.register_namespace(event_namespace)
-
+        self.sio.register_namespace(self.event_namespace)
         # Mount the socket app with the API.
         self.api.mount(str(constants.Endpoint.EVENT), self.socket_app)
 
@@ -592,9 +594,6 @@ def upload(app: App):
         Args:
             files: The file(s) to upload.
 
-        Returns:
-            The state update after processing the event.
-
         Raises:
             ValueError: if there are no args with supported annotation.
         """
@@ -603,10 +602,10 @@ def upload(app: App):
         for file in files:
             assert file.filename is not None
             file.filename = file.filename.split(":")[-1]
-
         # Get the state for the session.
         state = app.state_manager.get_state(token)
-
+        # get the current session ID
+        sid = state.get_sid()
         # get the current state(parent state/substate)
         path = handler.split(".")[:-1]
         current_state = state.get_substate(path)
@@ -636,12 +635,17 @@ def upload(app: App):
             name=handler,
             payload={handler_upload_param[0]: files},
         )
-        # TODO: refactor this to handle yields.
-        update = await state._process(event).__anext__()
-
+        async for update in state._process(event):
+            # Postprocess the event.
+            update = await app.postprocess(state, event, update)
+            # Send update to client
+            await asyncio.create_task(
+                app.event_namespace.emit(  # type: ignore
+                    str(constants.SocketEvent.EVENT), update.json(), to=sid
+                )
+            )
         # Set the state for the session.
         app.state_manager.set_state(event.token, state)
-        return update
 
     return upload_file
 

+ 17 - 9
tests/test_app.py

@@ -602,7 +602,7 @@ async def test_dict_mutation_detection__plain_list(
 
 @pytest.mark.asyncio
 @pytest.mark.parametrize(
-    "fixture, expected",
+    "fixture, delta",
     [
         (
             "upload_state",
@@ -626,22 +626,23 @@ async def test_dict_mutation_detection__plain_list(
         ),
     ],
 )
-async def test_upload_file(fixture, request, expected):
+async def test_upload_file(fixture, request, delta):
     """Test that file upload works correctly.
 
     Args:
         fixture: The state.
         request: Fixture request.
-        expected: Expected delta
+        delta: Expected delta
     """
+    app = App(state=request.getfixturevalue(fixture))
+    app.event_namespace.emit = AsyncMock()  # type: ignore
+    current_state = app.state_manager.get_state("token")
     data = b"This is binary data"
 
     # Create a binary IO object and write data to it
     bio = io.BytesIO()
     bio.write(data)
 
-    app = App(state=request.getfixturevalue(fixture))
-
     file1 = UploadFile(
         filename="token:file_upload_state.multi_handle_upload:True:image1.jpg",
         file=bio,
@@ -650,10 +651,17 @@ async def test_upload_file(fixture, request, expected):
         filename="token:file_upload_state.multi_handle_upload:True:image2.jpg",
         file=bio,
     )
-    fn = upload(app)
-    result = await fn([file1, file2])  # type: ignore
-    assert isinstance(result, StateUpdate)
-    assert result.delta == expected
+    upload_fn = upload(app)
+    await upload_fn([file1, file2])
+    state_update = StateUpdate(delta=delta, events=[], final=True)
+
+    app.event_namespace.emit.assert_called_with(  # type: ignore
+        "event", state_update.json(), to=current_state.get_sid()
+    )
+    assert app.state_manager.get_state("token").dict()["img_list"] == [
+        "image1.jpg",
+        "image2.jpg",
+    ]
 
 
 @pytest.mark.asyncio