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/starlite/app.py b/starlite/app.py
--- a/starlite/app.py
+++ b/starlite/app.py
@@ -16,6 +16,7 @@
 from starlite.config.app import AppConfig
 from starlite.config.response_cache import ResponseCacheConfig
 from starlite.connection import Request, WebSocket
+from starlite.constants import OPENAPI_NOT_INITIALIZED
 from starlite.datastructures.state import State
 from starlite.events.emitter import BaseEventEmitterBackend, SimpleEventEmitter
 from starlite.exceptions import (
@@ -458,17 +459,24 @@ async def __call__(
         await self.asgi_handler(scope, receive, self._wrap_send(send=send, scope=scope))  # type: ignore[arg-type]
 
     @property
-    def openapi_schema(self) -> OpenAPI | None:
+    def openapi_schema(self) -> OpenAPI:
         """Access  the OpenAPI schema of the application.
 
         Returns:
             The :class:`OpenAPI`
             <pydantic_openapi_schema.open_api.OpenAPI> instance of the
-            application's.
+            application.
+
+        Raises:
+            ImproperlyConfiguredException: If the application ``openapi_config`` attribute is ``None``.
         """
-        if self.openapi_config and not self._openapi_schema:
+        if not self.openapi_config:
+            raise ImproperlyConfiguredException(OPENAPI_NOT_INITIALIZED)
+
+        if not self._openapi_schema:
             self._openapi_schema = self.openapi_config.to_openapi_schema()
             self.update_openapi_schema()
+
         return self._openapi_schema
 
     @classmethod
diff --git a/starlite/constants.py b/starlite/constants.py
--- a/starlite/constants.py
+++ b/starlite/constants.py
@@ -16,3 +16,4 @@
 SCOPE_STATE_RESPONSE_COMPRESSED = "response_compressed"
 UNDEFINED_SENTINELS = {Undefined, Signature.empty, Empty, Ellipsis}
 SKIP_VALIDATION_NAMES = {"request", "socket", "scope", "receive", "send"}
+OPENAPI_NOT_INITIALIZED = "Starlite has not been instantiated with OpenAPIConfig"
diff --git a/starlite/openapi/controller.py b/starlite/openapi/controller.py
--- a/starlite/openapi/controller.py
+++ b/starlite/openapi/controller.py
@@ -6,6 +6,7 @@
 
 from yaml import dump as dump_yaml
 
+from starlite.constants import OPENAPI_NOT_INITIALIZED
 from starlite.controller import Controller
 from starlite.enums import MediaType, OpenAPIMediaType
 from starlite.exceptions import ImproperlyConfiguredException
@@ -21,8 +22,6 @@
     from starlite.connection.request import Request
     from starlite.openapi.spec.open_api import OpenAPI
 
-MSG_OPENAPI_NOT_INITIALIZED = "Starlite has not been instantiated with OpenAPIConfig"
-
 
 class OpenAPISchemaResponse(Response):
     """Response class for OpenAPI Schemas."""
@@ -98,12 +97,7 @@ def get_schema_from_request(request: Request) -> "OpenAPI":
 
         Returns:
             An :class:`OpenAPI <starlite.openapi.spec.open_api.OpenAPI>` instance.
-
-        Raises:
-            ImproperlyConfiguredException: If the application ``openapi_config`` attribute is ``None``.
         """
-        if not request.app.openapi_schema:  # pragma: no cover
-            raise ImproperlyConfiguredException(MSG_OPENAPI_NOT_INITIALIZED)
         return request.app.openapi_schema
 
     def should_serve_endpoint(self, request: "Request") -> bool:
@@ -173,9 +167,6 @@ def retrieve_schema_yaml(self, request: Request) -> Response:
         Returns:
             A Response instance with the YAML object rendered into a string.
         """
-        if not request.app.openapi_config:  # pragma: no cover
-            raise ImproperlyConfiguredException(MSG_OPENAPI_NOT_INITIALIZED)
-
         if self.should_serve_endpoint(request):
             return OpenAPISchemaResponse(
                 content=self.get_schema_from_request(request), media_type=OpenAPIMediaType.OPENAPI_YAML
@@ -193,9 +184,6 @@ def retrieve_schema_json(self, request: Request) -> Response:
         Returns:
             A Response instance with the JSON object rendered into a string.
         """
-        if not request.app.openapi_config:  # pragma: no cover
-            raise ImproperlyConfiguredException(MSG_OPENAPI_NOT_INITIALIZED)
-
         if self.should_serve_endpoint(request):
             return OpenAPISchemaResponse(
                 content=self.get_schema_from_request(request), media_type=OpenAPIMediaType.OPENAPI_JSON
@@ -221,7 +209,7 @@ def root(self, request: Request) -> Response:
         """
         config = request.app.openapi_config
         if not config:  # pragma: no cover
-            raise ImproperlyConfiguredException(MSG_OPENAPI_NOT_INITIALIZED)
+            raise ImproperlyConfiguredException(OPENAPI_NOT_INITIALIZED)
 
         render_method = self.render_methods_map[config.root_schema_site]
 
@@ -245,9 +233,6 @@ def swagger_ui(self, request: Request) -> Response:
         Returns:
             A response with a rendered swagger documentation site
         """
-        if not request.app.openapi_config:  # pragma: no cover
-            raise ImproperlyConfiguredException(MSG_OPENAPI_NOT_INITIALIZED)
-
         if self.should_serve_endpoint(request):
             return Response(content=self.render_swagger_ui(request), media_type=MediaType.HTML)
         return Response(
@@ -267,9 +252,6 @@ def stoplight_elements(self, request: Request) -> Response:
         Returns:
             A response with a rendered stoplight elements documentation site
         """
-        if not request.app.openapi_config:  # pragma: no cover
-            raise ImproperlyConfiguredException(MSG_OPENAPI_NOT_INITIALIZED)
-
         if self.should_serve_endpoint(request):
             return Response(content=self.render_stoplight_elements(request), media_type=MediaType.HTML)
         return Response(content=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)
@@ -285,9 +267,6 @@ def redoc(self, request: Request) -> Response:  # pragma: no cover
         Returns:
             A response with a rendered redoc documentation site
         """
-        if not request.app.openapi_config:  # pragma: no cover
-            raise ImproperlyConfiguredException(MSG_OPENAPI_NOT_INITIALIZED)
-
         if self.should_serve_endpoint(request):
             return Response(content=self.render_redoc(request), media_type=MediaType.HTML)
         return Response(content=self.render_404_page(), status_code=HTTP_404_NOT_FOUND, media_type=MediaType.HTML)


Test patch:
diff --git a/tests/app/test_app.py b/tests/app/test_app.py
new file mode 100644
--- /dev/null
+++ b/tests/app/test_app.py
@@ -0,0 +1,13 @@
+from __future__ import annotations
+
+import pytest
+
+from starlite import Starlite
+from starlite.exceptions import ImproperlyConfiguredException
+
+
+def test_access_openapi_schema_raises_if_not_configured() -> None:
+    """Test that accessing the openapi schema raises if not configured."""
+    app = Starlite(openapi_config=None)
+    with pytest.raises(ImproperlyConfiguredException):
+        app.openapi_schema
diff --git a/tests/contrib/jwt/test_auth.py b/tests/contrib/jwt/test_auth.py
--- a/tests/contrib/jwt/test_auth.py
+++ b/tests/contrib/jwt/test_auth.py
@@ -362,7 +362,7 @@ def login_custom_handler() -> Response["User"]:
     assert jwt_auth.security_requirement == {"BearerToken": []}
 
     app = Starlite(on_app_init=[jwt_auth.on_app_init])
-    assert app.openapi_schema.to_schema() == {  # type: ignore
+    assert app.openapi_schema.to_schema() == {
         "openapi": "3.1.0",
         "info": {"title": "Starlite API", "version": "1.0.0"},
         "servers": [{"url": "/"}],
diff --git a/tests/openapi/test_config.py b/tests/openapi/test_config.py
--- a/tests/openapi/test_config.py
+++ b/tests/openapi/test_config.py
@@ -83,7 +83,7 @@ def handler_2() -> None:
         openapi_config=OpenAPIConfig(title="my title", version="1.0.0", operation_id_creator=operation_id_creator),
     )
 
-    assert app.openapi_schema.to_schema()["paths"] == {  # type: ignore[union-attr]
+    assert app.openapi_schema.to_schema()["paths"] == {
         "/1": {
             "get": {
                 "deprecated": False,
diff --git a/tests/openapi/test_request_body.py b/tests/openapi/test_request_body.py
--- a/tests/openapi/test_request_body.py
+++ b/tests/openapi/test_request_body.py
@@ -49,7 +49,7 @@ async def handle_file_list_upload(
         return None
 
     app = Starlite(route_handlers=[handle_form_upload, handle_file_upload, handle_file_list_upload])
-    schema_dict = app.openapi_schema.to_schema()  # type: ignore[union-attr]
+    schema_dict = app.openapi_schema.to_schema()
     paths = schema_dict["paths"]
     components = schema_dict["components"]
     assert paths["/file-upload"]["post"]["requestBody"]["content"]["multipart/form-data"]["schema"] == {
diff --git a/tests/openapi/test_tags.py b/tests/openapi/test_tags.py
--- a/tests/openapi/test_tags.py
+++ b/tests/openapi/test_tags.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Any, Type, cast
+from typing import TYPE_CHECKING, Any, Type
 
 import pytest
 
@@ -43,7 +43,7 @@ def app(handler: HTTPRouteHandler, controller: Type[Controller], router: Router)
 
 @pytest.fixture()
 def openapi_schema(app: Starlite) -> "OpenAPI":
-    return cast("OpenAPI", app.openapi_schema)
+    return app.openapi_schema
 
 
 def test_openapi_schema_handler_tags(openapi_schema: "OpenAPI") -> None:
diff --git a/tests/security/test_session_auth.py b/tests/security/test_session_auth.py
--- a/tests/security/test_session_auth.py
+++ b/tests/security/test_session_auth.py
@@ -80,7 +80,7 @@ def test_session_auth_openapi(session_backend_config_memory: "ServerSideSessionC
         session_backend_config=session_backend_config_memory,
     )
     app = Starlite(on_app_init=[session_auth.on_app_init])
-    assert app.openapi_schema.to_schema() == {  # type: ignore
+    assert app.openapi_schema.to_schema() == {
         "openapi": "3.1.0",
         "info": {"title": "Starlite API", "version": "1.0.0"},
         "servers": [{"url": "/"}],