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/app.py b/litestar/app.py
--- a/litestar/app.py
+++ b/litestar/app.py
@@ -2,7 +2,6 @@
 
 import logging
 from datetime import date, datetime, time, timedelta
-from functools import partial
 from itertools import chain
 from pathlib import Path
 from typing import TYPE_CHECKING, Any, Literal, Mapping, Sequence, cast
@@ -12,7 +11,6 @@
 from litestar._asgi import ASGIRouter
 from litestar._asgi.utils import get_route_handlers, wrap_in_exception_handler
 from litestar._openapi.path_item import create_path_item
-from litestar._signature import create_signature_model
 from litestar.config.allowed_hosts import AllowedHostsConfig
 from litestar.config.app import AppConfig
 from litestar.config.response_cache import ResponseCacheConfig
@@ -24,7 +22,6 @@
     ImproperlyConfiguredException,
     NoRouteMatchFoundException,
 )
-from litestar.handlers.http_handlers import HTTPRouteHandler
 from litestar.logging.config import LoggingConfig, get_logger_placeholder
 from litestar.middleware.cors import CORSMiddleware
 from litestar.openapi.config import OpenAPIConfig
@@ -42,14 +39,10 @@
 from litestar.types.internal_types import PathParameterDefinition
 from litestar.utils import (
     as_async_callable_list,
-    async_partial,
-    is_async_callable,
     join_paths,
     unique,
 )
 from litestar.utils.dataclass import extract_dataclass_items
-from litestar.utils.helpers import unwrap_partial
-from litestar.utils.signature import ParsedSignature
 
 if TYPE_CHECKING:
     from litestar.config.compression import CompressionConfig
@@ -58,7 +51,6 @@
     from litestar.datastructures import CacheControlHeader, ETag, ResponseHeader
     from litestar.dto.interface import DTOInterface
     from litestar.events.listener import EventListener
-    from litestar.handlers.base import BaseRouteHandler
     from litestar.logging.config import BaseLoggingConfig
     from litestar.openapi.spec import SecurityRequirement
     from litestar.openapi.spec.open_api import OpenAPI
@@ -541,9 +533,7 @@ def register(self, value: ControllerRouterHandler) -> None:  # type: ignore[over
             route_handlers = get_route_handlers(route)
 
             for route_handler in route_handlers:
-                route_handler.on_registration()
-                self._set_runtime_callables(route_handler=route_handler)
-                self._create_handler_signature_model(route_handler=route_handler)
+                route_handler.on_registration(self)
 
             if isinstance(route, HTTPRoute):
                 route.create_handler_map()
@@ -720,63 +710,6 @@ def _create_asgi_handler(self) -> ASGIApp:
             debug=self.debug, app=asgi_handler, exception_handlers=self.exception_handlers or {}
         )
 
-    @staticmethod
-    def _set_runtime_callables(route_handler: BaseRouteHandler) -> None:
-        """Optimize the ``route_handler.fn`` and any ``provider.dependency`` callables for runtime by doing the following:
-
-        1. ensure that the ``self`` argument is preserved by binding it using partial.
-        2. ensure sync functions are wrapped in AsyncCallable for sync_to_thread handlers.
-
-        Args:
-            route_handler: A route handler to process.
-
-        Returns:
-            None
-        """
-        from litestar.controller import Controller
-
-        if isinstance(route_handler.owner, Controller) and not hasattr(route_handler.fn.value, "func"):
-            route_handler.fn.value = partial(route_handler.fn.value, route_handler.owner)
-
-        if isinstance(route_handler, HTTPRouteHandler):
-            route_handler.has_sync_callable = False
-            if not is_async_callable(route_handler.fn.value):
-                if route_handler.sync_to_thread:
-                    route_handler.fn.value = async_partial(route_handler.fn.value)
-                else:
-                    route_handler.has_sync_callable = True
-
-        for provider in route_handler.resolve_dependencies().values():
-            if not is_async_callable(provider.dependency.value):
-                provider.has_sync_callable = False
-                if provider.sync_to_thread:
-                    provider.dependency.value = async_partial(provider.dependency.value)
-                else:
-                    provider.has_sync_callable = True
-
-    def _create_handler_signature_model(self, route_handler: BaseRouteHandler) -> None:
-        """Create function signature models for all route handler functions and provider dependencies."""
-        if not route_handler.signature_model:
-            route_handler.signature_model = create_signature_model(
-                dependency_name_set=route_handler.dependency_name_set,
-                fn=cast("AnyCallable", route_handler.fn.value),
-                plugins=self.serialization_plugins,
-                preferred_validation_backend=self.preferred_validation_backend,
-                parsed_signature=route_handler.parsed_fn_signature,
-            )
-
-        for provider in route_handler.resolve_dependencies().values():
-            if not getattr(provider, "signature_model", None):
-                provider.signature_model = create_signature_model(
-                    dependency_name_set=route_handler.dependency_name_set,
-                    fn=provider.dependency.value,
-                    plugins=self.serialization_plugins,
-                    preferred_validation_backend=self.preferred_validation_backend,
-                    parsed_signature=ParsedSignature.from_fn(
-                        unwrap_partial(provider.dependency.value), route_handler.resolve_signature_namespace()
-                    ),
-                )
-
     def _wrap_send(self, send: Send, scope: Scope) -> Send:
         """Wrap the ASGI send and handles any 'before send' hooks.
 
diff --git a/litestar/handlers/base.py b/litestar/handlers/base.py
--- a/litestar/handlers/base.py
+++ b/litestar/handlers/base.py
@@ -1,26 +1,29 @@
 from __future__ import annotations
 
 from copy import copy
+from functools import partial
 from typing import TYPE_CHECKING, Any, Generic, Mapping, Sequence, TypeVar, cast
 
+from litestar._signature import create_signature_model
 from litestar._signature.field import SignatureField
 from litestar.dto.interface import DTOInterface
 from litestar.exceptions import ImproperlyConfiguredException
 from litestar.types import Dependencies, Empty, ExceptionHandlersMap, Guard, Middleware, TypeEncodersMap
-from litestar.utils import AsyncCallable, Ref, get_name, normalize_path
+from litestar.utils import AsyncCallable, Ref, async_partial, get_name, is_async_callable, normalize_path
 from litestar.utils.helpers import unwrap_partial
 from litestar.utils.signature import ParsedSignature
 
 if TYPE_CHECKING:
     from typing_extensions import Self
 
+    from litestar import Litestar
     from litestar._signature.models import SignatureModel
     from litestar.connection import ASGIConnection
     from litestar.controller import Controller
     from litestar.di import Provide
     from litestar.params import ParameterKwarg
     from litestar.router import Router
-    from litestar.types import AsyncAnyCallable, ExceptionHandler
+    from litestar.types import AnyCallable, AsyncAnyCallable, ExceptionHandler
     from litestar.types.composite_types import MaybePartial
     from litestar.types.empty import EmptyType
 
@@ -369,17 +372,61 @@ def _validate_dependency_is_unique(dependencies: dict[str, Provide], key: str, p
                     f"If you wish to override a provider, it must have the same key."
                 )
 
-    def on_registration(self) -> None:
+    def on_registration(self, app: Litestar) -> None:
         """Called once per handler when the app object is instantiated."""
         self._validate_handler_function()
         self.resolve_guards()
         self.resolve_middleware()
         self.resolve_opts()
         self._init_handler_dtos()
+        self._set_runtime_callables()
+        self._create_signature_model(app)
 
     def _validate_handler_function(self) -> None:
         """Validate the route handler function once set by inspecting its return annotations."""
 
+    def _set_runtime_callables(self) -> None:
+        """Optimize the ``route_handler.fn`` and any ``provider.dependency`` callables for runtime by doing the following:
+
+        1. ensure that the ``self`` argument is preserved by binding it using partial.
+        2. ensure sync functions are wrapped in AsyncCallable for sync_to_thread handlers.
+        """
+        from litestar.controller import Controller
+
+        if isinstance(self.owner, Controller) and not hasattr(self.fn.value, "func"):
+            self.fn.value = partial(self.fn.value, self.owner)
+
+        for provider in self.resolve_dependencies().values():
+            if not is_async_callable(provider.dependency.value):
+                provider.has_sync_callable = False
+                if provider.sync_to_thread:
+                    provider.dependency.value = async_partial(provider.dependency.value)
+                else:
+                    provider.has_sync_callable = True
+
+    def _create_signature_model(self, app: Litestar) -> None:
+        """Create signature model for handler function and dependencies."""
+        if not self.signature_model:
+            self.signature_model = create_signature_model(
+                dependency_name_set=self.dependency_name_set,
+                fn=cast("AnyCallable", self.fn.value),
+                plugins=app.serialization_plugins,
+                preferred_validation_backend=app.preferred_validation_backend,
+                parsed_signature=self.parsed_fn_signature,
+            )
+
+        for provider in self.resolve_dependencies().values():
+            if not getattr(provider, "signature_model", None):
+                provider.signature_model = create_signature_model(
+                    dependency_name_set=self.dependency_name_set,
+                    fn=provider.dependency.value,
+                    plugins=app.serialization_plugins,
+                    preferred_validation_backend=app.preferred_validation_backend,
+                    parsed_signature=ParsedSignature.from_fn(
+                        unwrap_partial(provider.dependency.value), self.resolve_signature_namespace()
+                    ),
+                )
+
     def __str__(self) -> str:
         """Return a unique identifier for the route handler.
 
diff --git a/litestar/handlers/http_handlers/base.py b/litestar/handlers/http_handlers/base.py
--- a/litestar/handlers/http_handlers/base.py
+++ b/litestar/handlers/http_handlers/base.py
@@ -44,7 +44,7 @@
     TypeEncodersMap,
 )
 from litestar.types.builtin_types import NoneType
-from litestar.utils import AsyncCallable, is_async_callable
+from litestar.utils import AsyncCallable, async_partial, is_async_callable
 
 if TYPE_CHECKING:
     from typing import Any, Awaitable, Callable, Sequence
@@ -468,8 +468,8 @@ async def to_response(
         response_handler = self.get_response_handler(is_response_type_data=isinstance(data, Response))
         return await response_handler(app=app, data=data, plugins=plugins, request=request, return_dto=self.resolve_return_dto())  # type: ignore
 
-    def on_registration(self) -> None:
-        super().on_registration()
+    def on_registration(self, app: Litestar) -> None:
+        super().on_registration(app)
         if before_request := self.resolve_before_request():
             before_request.set_parsed_signature(self.resolve_signature_namespace())
         self.resolve_after_response()
@@ -508,5 +508,15 @@ def _validate_handler_function(self) -> None:
         if "data" in self.parsed_fn_signature.parameters and "GET" in self.http_methods:
             raise ImproperlyConfiguredException("'data' kwarg is unsupported for 'GET' request handlers")
 
+    def _set_runtime_callables(self) -> None:
+        """Set the runtime callables for the route handler."""
+        super()._set_runtime_callables()
+        self.has_sync_callable = False
+        if not is_async_callable(self.fn.value):
+            if self.sync_to_thread:
+                self.fn.value = async_partial(self.fn.value)
+            else:
+                self.has_sync_callable = True
+
 
 route = HTTPRouteHandler


Test patch:
diff --git a/tests/handlers/asgi/test_validations.py b/tests/handlers/asgi/test_validations.py
--- a/tests/handlers/asgi/test_validations.py
+++ b/tests/handlers/asgi/test_validations.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from litestar import asgi
+from litestar import Litestar, asgi
 from litestar.exceptions import ImproperlyConfiguredException
 from litestar.testing import create_test_client
 
@@ -15,25 +15,25 @@ async def fn_without_scope_arg(receive: "Receive", send: "Send") -> None:
         pass
 
     with pytest.raises(ImproperlyConfiguredException):
-        asgi(path="/")(fn_without_scope_arg).on_registration()
+        asgi(path="/")(fn_without_scope_arg).on_registration(Litestar())
 
     async def fn_without_receive_arg(scope: "Scope", send: "Send") -> None:
         pass
 
     with pytest.raises(ImproperlyConfiguredException):
-        asgi(path="/")(fn_without_receive_arg).on_registration()
+        asgi(path="/")(fn_without_receive_arg).on_registration(Litestar())
 
     async def fn_without_send_arg(scope: "Scope", receive: "Receive") -> None:
         pass
 
     with pytest.raises(ImproperlyConfiguredException):
-        asgi(path="/")(fn_without_send_arg).on_registration()
+        asgi(path="/")(fn_without_send_arg).on_registration(Litestar())
 
     async def fn_with_return_annotation(scope: "Scope", receive: "Receive", send: "Send") -> dict:
         return {}
 
     with pytest.raises(ImproperlyConfiguredException):
-        asgi(path="/")(fn_with_return_annotation).on_registration()
+        asgi(path="/")(fn_with_return_annotation).on_registration(Litestar())
 
     asgi_handler_with_no_fn = asgi(path="/")
 
@@ -44,4 +44,4 @@ def sync_fn(scope: "Scope", receive: "Receive", send: "Send") -> None:
         return None
 
     with pytest.raises(ImproperlyConfiguredException):
-        asgi(path="/")(sync_fn).on_registration()  # type: ignore
+        asgi(path="/")(sync_fn).on_registration(Litestar())  # type: ignore
diff --git a/tests/handlers/http/test_head.py b/tests/handlers/http/test_head.py
--- a/tests/handlers/http/test_head.py
+++ b/tests/handlers/http/test_head.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from litestar import HttpMethod, head
+from litestar import HttpMethod, Litestar, head
 from litestar.exceptions import ImproperlyConfiguredException
 from litestar.response import FileResponse
 from litestar.response_containers import File
@@ -27,7 +27,7 @@ def test_head_decorator_raises_validation_error_if_body_is_declared() -> None:
         def handler() -> dict:
             return {}
 
-        handler.on_registration()
+        handler.on_registration(Litestar())
 
 
 def test_head_decorator_raises_validation_error_if_method_is_passed() -> None:
@@ -37,7 +37,7 @@ def test_head_decorator_raises_validation_error_if_method_is_passed() -> None:
         def handler() -> None:
             return
 
-        handler.on_registration()
+        handler.on_registration(Litestar())
 
 
 def test_head_decorator_does_not_raise_for_file() -> None:
@@ -45,7 +45,7 @@ def test_head_decorator_does_not_raise_for_file() -> None:
     def handler() -> File:
         return File(path=Path("test_head.py"))
 
-    handler.on_registration()
+    handler.on_registration(Litestar())
 
 
 def test_head_decorator_does_not_raise_for_file_response() -> None:
@@ -53,4 +53,4 @@ def test_head_decorator_does_not_raise_for_file_response() -> None:
     def handler() -> "FileResponse":
         return FileResponse("test_to_response.py")
 
-    handler.on_registration()
+    handler.on_registration(Litestar())
diff --git a/tests/handlers/http/test_media_type.py b/tests/handlers/http/test_media_type.py
--- a/tests/handlers/http/test_media_type.py
+++ b/tests/handlers/http/test_media_type.py
@@ -4,7 +4,7 @@
 import pytest
 from pydantic.types import PaymentCardBrand
 
-from litestar import MediaType, get
+from litestar import Litestar, MediaType, get
 from tests import Person
 
 
@@ -34,5 +34,5 @@ def test_media_type_inference(annotation: Any, expected_media_type: MediaType) -
     def handler() -> annotation:
         return None
 
-    handler.on_registration()
+    handler.on_registration(Litestar())
     assert handler.media_type == expected_media_type
diff --git a/tests/handlers/http/test_validations.py b/tests/handlers/http/test_validations.py
--- a/tests/handlers/http/test_validations.py
+++ b/tests/handlers/http/test_validations.py
@@ -3,7 +3,7 @@
 
 import pytest
 
-from litestar import HttpMethod, MediaType, WebSocket, delete, get, route
+from litestar import HttpMethod, Litestar, MediaType, WebSocket, delete, get, route
 from litestar.exceptions import ImproperlyConfiguredException, ValidationException
 from litestar.handlers.http_handlers import HTTPRouteHandler
 from litestar.response_containers import File, Redirect
@@ -41,7 +41,7 @@ async def test_function_validation() -> None:
         def method_with_no_annotation():  # type: ignore
             pass
 
-        method_with_no_annotation.on_registration()
+        method_with_no_annotation.on_registration(Litestar())
 
     with pytest.raises(ImproperlyConfiguredException):
 
@@ -49,7 +49,7 @@ def method_with_no_annotation():  # type: ignore
         def method_with_no_content() -> Dict[str, str]:
             return {}
 
-        method_with_no_content.on_registration()
+        method_with_no_content.on_registration(Litestar())
 
     with pytest.raises(ImproperlyConfiguredException):
 
@@ -57,7 +57,7 @@ def method_with_no_content() -> Dict[str, str]:
         def method_with_not_modified() -> Dict[str, str]:
             return {}
 
-        method_with_not_modified.on_registration()
+        method_with_not_modified.on_registration(Litestar())
 
     with pytest.raises(ImproperlyConfiguredException):
 
@@ -65,19 +65,19 @@ def method_with_not_modified() -> Dict[str, str]:
         def method_with_status_lower_than_200() -> Dict[str, str]:
             return {}
 
-        method_with_status_lower_than_200.on_registration()
+        method_with_status_lower_than_200.on_registration(Litestar())
 
     @get(path="/", status_code=HTTP_307_TEMPORARY_REDIRECT)
     def redirect_method() -> Redirect:
         return Redirect("/test")
 
-    redirect_method.on_registration()
+    redirect_method.on_registration(Litestar())
 
     @get(path="/")
     def file_method() -> File:
         return File(path=Path("."), filename="test_validations.py")
 
-    file_method.on_registration()
+    file_method.on_registration(Litestar())
 
     assert file_method.media_type == MediaType.TEXT
 
@@ -87,7 +87,7 @@ def file_method() -> File:
         def test_function_1(socket: WebSocket) -> None:
             return None
 
-        test_function_1.on_registration()
+        test_function_1.on_registration(Litestar())
 
     with pytest.raises(ImproperlyConfiguredException):
 
@@ -95,4 +95,4 @@ def test_function_1(socket: WebSocket) -> None:
         def test_function_2(self, data: Person) -> None:  # type: ignore
             return None
 
-        test_function_2.on_registration()
+        test_function_2.on_registration(Litestar())
diff --git a/tests/handlers/websocket/test_validations.py b/tests/handlers/websocket/test_validations.py
--- a/tests/handlers/websocket/test_validations.py
+++ b/tests/handlers/websocket/test_validations.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from litestar import WebSocket, websocket
+from litestar import Litestar, WebSocket, websocket
 from litestar.exceptions import ImproperlyConfiguredException
 from litestar.testing import create_test_client
 
@@ -12,7 +12,7 @@ def fn_without_socket_arg(websocket: WebSocket) -> None:
         pass
 
     with pytest.raises(ImproperlyConfiguredException):
-        websocket(path="/")(fn_without_socket_arg).on_registration()  # type: ignore
+        websocket(path="/")(fn_without_socket_arg).on_registration(Litestar())  # type: ignore
 
 
 def test_raises_for_return_annotation() -> None:
@@ -20,7 +20,7 @@ async def fn_with_return_annotation(socket: WebSocket) -> dict:
         return {}
 
     with pytest.raises(ImproperlyConfiguredException):
-        websocket(path="/")(fn_with_return_annotation).on_registration()
+        websocket(path="/")(fn_with_return_annotation).on_registration(Litestar())
 
 
 def test_raises_when_no_function() -> None:
@@ -37,7 +37,7 @@ def test_raises_when_sync_handler_user() -> None:
         def sync_websocket_handler(socket: WebSocket) -> None:
             ...
 
-        sync_websocket_handler.on_registration()
+        sync_websocket_handler.on_registration(Litestar())
 
 
 def test_raises_when_data_kwarg_is_used() -> None:
@@ -47,7 +47,7 @@ def test_raises_when_data_kwarg_is_used() -> None:
         async def websocket_handler_with_data_kwarg(socket: WebSocket, data: Any) -> None:
             ...
 
-        websocket_handler_with_data_kwarg.on_registration()
+        websocket_handler_with_data_kwarg.on_registration(Litestar())
 
 
 def test_raises_when_request_kwarg_is_used() -> None:
@@ -57,7 +57,7 @@ def test_raises_when_request_kwarg_is_used() -> None:
         async def websocket_handler_with_request_kwarg(socket: WebSocket, request: Any) -> None:
             ...
 
-        websocket_handler_with_request_kwarg.on_registration()
+        websocket_handler_with_request_kwarg.on_registration(Litestar())
 
 
 def test_raises_when_body_kwarg_is_used() -> None:
@@ -67,4 +67,4 @@ def test_raises_when_body_kwarg_is_used() -> None:
         async def websocket_handler_with_request_kwarg(socket: WebSocket, body: bytes) -> None:
             ...
 
-        websocket_handler_with_request_kwarg.on_registration()
+        websocket_handler_with_request_kwarg.on_registration(Litestar())