Repository: litestar-org/litestar

Issue:
StaticFilesConfig and virtual directories
I'm trying to write a ``FileSystemProtocol`` to load files from the package data using [importlib_resources](https://importlib-resources.readthedocs.io/en/latest/using.html#). But because ``directories`` is defined as ``DirectoryPath``, pydantic checks if the given directories exist in the local filesystem. 

This is not generally true, especially in any kind of virtual filesystem (e.g. a zipped package). I think this condition should be relaxed to support virtual filesystems.

https://github.com/starlite-api/starlite/blob/9bb6dcd57c10a591377cf8e3a537e9292566d5b9/starlite/config/static_files.py#L32


Discussion:
I agree. Do you want to submit a PR?

Resolving patch:
diff --git a/litestar/connection/websocket.py b/litestar/connection/websocket.py
--- a/litestar/connection/websocket.py
+++ b/litestar/connection/websocket.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, Generic, Literal, cast, overload
+from typing import TYPE_CHECKING, Any, AsyncGenerator, Generic, Literal, cast, overload
 
 from litestar.connection.base import (
     ASGIConnection,
@@ -12,7 +12,7 @@
 )
 from litestar.datastructures.headers import Headers
 from litestar.exceptions import WebSocketDisconnect, WebSocketException
-from litestar.serialization import decode_json, default_serializer, encode_json
+from litestar.serialization import decode_json, decode_msgpack, default_serializer, encode_json, encode_msgpack
 from litestar.status_codes import WS_1000_NORMAL_CLOSURE
 
 __all__ = ("WebSocket",)
@@ -29,6 +29,7 @@
         WebSocketAcceptEvent,
         WebSocketCloseEvent,
         WebSocketDisconnectEvent,
+        WebSocketMode,
         WebSocketReceiveEvent,
         WebSocketSendEvent,
     )
@@ -152,7 +153,7 @@ async def receive_data(self, mode: Literal["text"]) -> str:
     async def receive_data(self, mode: Literal["binary"]) -> bytes:
         ...
 
-    async def receive_data(self, mode: Literal["binary", "text"]) -> str | bytes:
+    async def receive_data(self, mode: WebSocketMode) -> str | bytes:
         """Receive an 'websocket.receive' event and returns the data stored on it.
 
         Args:
@@ -170,6 +171,26 @@ async def receive_data(self, mode: Literal["binary", "text"]) -> str | bytes:
             raise WebSocketDisconnect(detail=DISCONNECT_MESSAGE)  # pragma: no cover
         return event.get("text") or "" if mode == "text" else event.get("bytes") or b""
 
+    @overload
+    def iter_data(self, mode: Literal["text"]) -> AsyncGenerator[str, None]:
+        ...
+
+    @overload
+    def iter_data(self, mode: Literal["binary"]) -> AsyncGenerator[bytes, None]:
+        ...
+
+    async def iter_data(self, mode: WebSocketMode) -> AsyncGenerator[str | bytes, None]:
+        """Continuously receive data and yield it
+
+        Args:
+            mode: Socket mode to use. Either ``text`` or ``binary``
+        """
+        try:
+            while True:
+                yield await self.receive_data(mode)
+        except WebSocketDisconnect:
+            pass
+
     async def receive_text(self) -> str:
         """Receive data as text.
 
@@ -186,11 +207,8 @@ async def receive_bytes(self) -> bytes:
         """
         return await self.receive_data(mode="binary")
 
-    async def receive_json(
-        self,
-        mode: Literal["text", "binary"] = "text",
-    ) -> Any:
-        """Receive data and loads it into JSON using orson.
+    async def receive_json(self, mode: WebSocketMode = "text") -> Any:
+        """Receive data and decode it as JSON.
 
         Args:
             mode: Either ``text`` or ``binary``.
@@ -201,9 +219,39 @@ async def receive_json(
         data = await self.receive_data(mode=mode)
         return decode_json(data)
 
-    async def send_data(
-        self, data: str | bytes, mode: Literal["text", "binary"] = "text", encoding: str = "utf-8"
-    ) -> None:
+    async def receive_msgpack(self) -> Any:
+        """Receive data and decode it as MessagePack.
+
+        Note that since MessagePack is a binary format, this method will always receive
+        data in ``binary`` mode.
+
+        Returns:
+            An arbitrary value
+        """
+        data = await self.receive_data(mode="binary")
+        return decode_msgpack(data)
+
+    async def iter_json(self, mode: WebSocketMode) -> AsyncGenerator[Any, None]:
+        """Continuously receive data and yield it, decoding it as JSON in the process.
+
+        Args:
+            mode: Socket mode to use. Either ``text`` or ``binary``
+        """
+        async for data in self.iter_data(mode):
+            yield decode_json(data)
+
+    async def iter_msgpack(self) -> AsyncGenerator[Any, None]:
+        """Continuously receive data and yield it, decoding it as MessagePack in the
+        process.
+
+        Note that since MessagePack is a binary format, this method will always receive
+        data in ``binary`` mode.
+
+        """
+        async for data in self.iter_data(mode="binary"):
+            yield decode_msgpack(data)
+
+    async def send_data(self, data: str | bytes, mode: WebSocketMode = "text", encoding: str = "utf-8") -> None:
         """Send a 'websocket.send' event.
 
         Args:
@@ -266,7 +314,7 @@ async def send_bytes(self, data: str | bytes, encoding: str = "utf-8") -> None:
     async def send_json(
         self,
         data: Any,
-        mode: Literal["text", "binary"] = "text",
+        mode: WebSocketMode = "text",
         encoding: str = "utf-8",
         serializer: Serializer = default_serializer,
     ) -> None:
@@ -281,8 +329,25 @@ async def send_json(
         Returns:
             None
         """
-        await self.send_data(
-            data=encode_json(data, serializer),
-            mode=mode,
-            encoding=encoding,
-        )
+        await self.send_data(data=encode_json(data, serializer), mode=mode, encoding=encoding)
+
+    async def send_msgpack(
+        self,
+        data: Any,
+        encoding: str = "utf-8",
+        serializer: Serializer = default_serializer,
+    ) -> None:
+        """Send data as MessagePack.
+
+        Note that since MessagePack is a binary format, this method will always send
+        data in ``binary`` mode.
+
+        Args:
+            data: A value to serialize.
+            encoding: Encoding to use for binary data.
+            serializer: A serializer function.
+
+        Returns:
+            None
+        """
+        await self.send_data(data=encode_msgpack(data, serializer), mode="binary", encoding=encoding)


Test patch:
diff --git a/litestar/testing/websocket_test_session.py b/litestar/testing/websocket_test_session.py
--- a/litestar/testing/websocket_test_session.py
+++ b/litestar/testing/websocket_test_session.py
@@ -7,7 +7,7 @@
 from anyio import sleep
 
 from litestar.exceptions import WebSocketDisconnect
-from litestar.serialization import decode_json, encode_json
+from litestar.serialization import decode_json, decode_msgpack, encode_json, encode_msgpack
 from litestar.status_codes import WS_1000_NORMAL_CLOSURE
 
 if TYPE_CHECKING:
@@ -146,10 +146,21 @@ def send_json(self, data: Any, mode: Literal["text", "binary"] = "text") -> None
             mode: Either ``text`` or ``binary``
 
         Returns:
-            None.
+            None
         """
         self.send(encode_json(data), mode=mode)
 
+    def send_msgpack(self, data: Any) -> None:
+        """Sends the given data as MessagePack.
+
+        Args:
+            data: The data to send.
+
+        Returns:
+            None
+        """
+        self.send(encode_msgpack(data), mode="binary")
+
     def close(self, code: int = WS_1000_NORMAL_CLOSURE) -> None:
         """Sends an 'websocket.disconnect' event.
 
@@ -232,3 +243,7 @@ def receive_json(
             return decode_json(cast("str", message.get("text", "")))
 
         return decode_json(cast("bytes", message.get("bytes", b"")))
+
+    def receive_msgpack(self, block: bool = True, timeout: float | None = None) -> Any:
+        message = self.receive(block=block, timeout=timeout)
+        return decode_msgpack(cast("bytes", message.get("bytes", b"")))
diff --git a/tests/connection/websocket/test_websocket.py b/tests/connection/test_websocket.py
similarity index 70%
rename from tests/connection/websocket/test_websocket.py
rename to tests/connection/test_websocket.py
--- a/tests/connection/websocket/test_websocket.py
+++ b/tests/connection/test_websocket.py
@@ -1,10 +1,11 @@
-"""A large part of the tests in this file were adapted from:
-
-https://github.com/encode/starlette/blob/master/tests/test_websockets.py And are meant to ensure our compatibility with
-their API.
 """
+Some tests in this file were adapted from: https://github.com/encode/starlette/blob/master/tests/test_websockets.py And
+were meant to ensure our compatibility with their API.
+"""
+from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any, Literal
+from typing import TYPE_CHECKING, Any, AsyncGenerator, Literal
+from unittest.mock import MagicMock
 
 import anyio
 import pytest
@@ -15,6 +16,8 @@
 from litestar.handlers.websocket_handlers import websocket
 from litestar.status_codes import WS_1001_GOING_AWAY
 from litestar.testing import TestClient, create_test_client
+from litestar.types.asgi_types import WebSocketMode
+from litestar.utils.compat import async_next
 
 if TYPE_CHECKING:
     from litestar.types import Receive, Scope, Send
@@ -70,7 +73,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
             super().__init__(*args, **kwargs)
             self.scope["called"] = True  # type: ignore
 
-    @websocket("/")
+    @websocket("/", signature_namespace={"MyWebSocket": MyWebSocket})
     async def handler(socket: MyWebSocket) -> None:
         value["called"] = socket.scope.get("called")
         await socket.accept()
@@ -81,7 +84,7 @@ async def handler(socket: MyWebSocket) -> None:
 
 
 def test_websocket_url() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
         await websocket.send_json({"url": str(websocket.url)})
@@ -93,7 +96,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_websocket_binary_json() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
         message = await websocket.receive_json(mode="binary")
@@ -107,7 +110,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_websocket_query_params() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         query_params = dict(websocket.query_params)
         await websocket.accept()
@@ -120,7 +123,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_websocket_headers() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         headers = dict(websocket.headers)
         await websocket.accept()
@@ -142,7 +145,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_websocket_port() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
         await websocket.send_json({"port": websocket.url.port})
@@ -154,7 +157,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_websocket_send_and_receive_text() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
         data = await websocket.receive_text()
@@ -168,7 +171,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_websocket_send_and_receive_bytes() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
         data = await websocket.receive_bytes()
@@ -182,7 +185,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_websocket_send_and_receive_json() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
         data = await websocket.receive_json()
@@ -195,6 +198,100 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
         assert data == {"message": {"hello": "world"}}
 
 
+def test_send_msgpack() -> None:
+    test_data = {"message": "hello, world"}
+
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
+        socket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
+        await socket.accept()
+        await socket.send_msgpack(test_data)
+        await socket.close()
+
+    with TestClient(app).websocket_connect("/") as ws:
+        data = ws.receive_msgpack(timeout=1)
+        assert data == test_data
+
+
+def test_receive_msgpack() -> None:
+    test_data = {"message": "hello, world"}
+    callback = MagicMock()
+
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
+        socket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
+        await socket.accept()
+        data = await socket.receive_msgpack()
+        callback(data)
+        await socket.close()
+
+    with TestClient(app).websocket_connect("/") as ws:
+        ws.send_msgpack(test_data)
+
+    callback.assert_called_once_with(test_data)
+
+
+async def consume_gen(generator: AsyncGenerator[Any, Any], count: int, timeout: int = 1) -> list[Any]:
+    async def consumer() -> list[Any]:
+        result = []
+        for _ in range(count):
+            result.append(await async_next(generator))
+        return result
+
+    with anyio.fail_after(timeout):
+        return await consumer()
+
+
+@pytest.mark.parametrize("mode,data", [("text", ["foo", "bar"]), ("binary", [b"foo", b"bar"])])
+def test_iter_data(mode: WebSocketMode, data: list[str | bytes]) -> None:
+    values = []
+
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
+        socket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
+        await socket.accept()
+        values.extend(await consume_gen(socket.iter_data(mode=mode), 2))
+        await socket.close()
+
+    with TestClient(app).websocket_connect("/") as ws:
+        for message in data:
+            ws.send(message, mode=mode)
+
+    assert values == data
+
+
+@pytest.mark.parametrize("mode", ["text", "binary"])
+def test_iter_json(mode: WebSocketMode) -> None:
+    messages = [{"data": "foo"}, {"data": "bar"}]
+    values = []
+
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
+        socket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
+        await socket.accept()
+        values.extend(await consume_gen(socket.iter_json(mode=mode), 2))
+        await socket.close()
+
+    with TestClient(app).websocket_connect("/") as ws:
+        for message in messages:
+            ws.send_json(message, mode=mode)
+
+    assert values == messages
+
+
+def test_iter_msgpack() -> None:
+    messages = [{"data": "foo"}, {"data": "bar"}]
+    values = []
+
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
+        socket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
+        await socket.accept()
+        values.extend(await consume_gen(socket.iter_msgpack(), 2))
+        await socket.close()
+
+    with TestClient(app).websocket_connect("/") as ws:
+        for message in messages:
+            ws.send_msgpack(message)
+
+    assert values == messages
+
+
 def test_websocket_concurrency_pattern() -> None:
     stream_send, stream_receive = anyio.create_memory_object_stream()
 
@@ -208,7 +305,7 @@ async def writer(websocket: WebSocket[Any, Any, Any]) -> None:
             async for message in stream_receive:
                 await websocket.send_json(message)
 
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
         async with anyio.create_task_group() as task_group:
@@ -225,7 +322,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 def test_client_close() -> None:
     close_code = None
 
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         nonlocal close_code
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
@@ -240,7 +337,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_application_close() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
         await websocket.close(WS_1001_GOING_AWAY)
@@ -251,7 +348,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_rejected_connection() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.close(WS_1001_GOING_AWAY)
 
@@ -261,7 +358,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_subprotocol() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         assert websocket.scope["subprotocols"] == ["soap", "wamp"]
         await websocket.accept(subprotocols="wamp")
@@ -272,7 +369,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_additional_headers() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept(headers=[(b"additional", b"header")])
         await websocket.close()
@@ -282,7 +379,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_no_additional_headers() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
         await websocket.close()
@@ -292,7 +389,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_websocket_exception() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         raise RuntimeError
 
     with pytest.raises(RuntimeError), TestClient(app).websocket_connect("/123?a=abc"):
@@ -300,7 +397,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_duplicate_disconnect() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
         message = await websocket.receive()
@@ -312,7 +409,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_websocket_close_reason() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.accept()
         await websocket.close(code=WS_1001_GOING_AWAY, reason="Going Away")
@@ -324,7 +421,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_receive_text_before_accept() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.receive_text()
 
@@ -333,7 +430,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_receive_bytes_before_accept() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.receive_bytes()
 
@@ -342,7 +439,7 @@ async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
 
 
 def test_receive_json_before_accept() -> None:
-    async def app(scope: "Scope", receive: "Receive", send: "Send") -> None:
+    async def app(scope: Scope, receive: Receive, send: Send) -> None:
         websocket = WebSocket[Any, Any, Any](scope, receive=receive, send=send)
         await websocket.receive_json()
 
diff --git a/tests/connection/websocket/__init__.py b/tests/connection/websocket/__init__.py
deleted file mode 100644