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
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import logging
 from datetime import date, datetime, time, timedelta
 from functools import partial
 from itertools import chain
@@ -132,6 +133,7 @@ class Litestar(Router):
     """
 
     __slots__ = (
+        "_debug",
         "_openapi_schema",
         "after_exception",
         "after_shutdown",
@@ -145,7 +147,6 @@ class Litestar(Router):
         "compression_config",
         "cors_config",
         "csrf_config",
-        "debug",
         "event_emitter",
         "get_logger",
         "logger",
@@ -373,6 +374,7 @@ def __init__(
             config = handler(config)
 
         self._openapi_schema: OpenAPI | None = None
+        self._debug: bool = True
         self.get_logger: GetLogger = get_logger_placeholder
         self.logger: Logger | None = None
         self.routes: list[HTTPRoute | ASGIRoute | WebSocketRoute] = []
@@ -389,7 +391,6 @@ def __init__(
         self.compression_config = config.compression_config
         self.cors_config = config.cors_config
         self.csrf_config = config.csrf_config
-        self.debug = config.debug
         self.event_emitter = config.event_emitter_backend(listeners=config.listeners)
         self.logging_config = config.logging_config
         self.multipart_form_part_limit = config.multipart_form_part_limit
@@ -405,6 +406,7 @@ def __init__(
         self.static_files_config = config.static_files_config
         self.template_engine = config.template_config.engine_instance if config.template_config else None
         self.websocket_class = config.websocket_class or WebSocket
+        self.debug = config.debug
 
         super().__init__(
             after_request=config.after_request,
@@ -435,9 +437,6 @@ def __init__(
         for route_handler in config.route_handlers:
             self.register(route_handler)
 
-        if self.debug and isinstance(self.logging_config, LoggingConfig):
-            self.logging_config.loggers["litestar"]["level"] = "DEBUG"
-
         if self.logging_config:
             self.get_logger = self.logging_config.configure()
             self.logger = self.get_logger("litestar")
@@ -450,7 +449,21 @@ def __init__(
 
         self.asgi_handler = self._create_asgi_handler()
 
-        self.stores = config.stores if isinstance(config.stores, StoreRegistry) else StoreRegistry(config.stores)
+        self.stores: StoreRegistry = (
+            config.stores if isinstance(config.stores, StoreRegistry) else StoreRegistry(config.stores)
+        )
+
+    @property
+    def debug(self) -> bool:
+        return self._debug
+
+    @debug.setter
+    def debug(self, value: bool) -> None:
+        if self.logger:
+            self.logger.setLevel(logging.DEBUG if value else logging.INFO)
+        if isinstance(self.logging_config, LoggingConfig):
+            self.logging_config.loggers["litestar"]["level"] = "DEBUG" if value else "INFO"
+        self._debug = value
 
     async def __call__(
         self,
diff --git a/litestar/types/protocols.py b/litestar/types/protocols.py
--- a/litestar/types/protocols.py
+++ b/litestar/types/protocols.py
@@ -79,6 +79,16 @@ def critical(self, event: str, *args: Any, **kwargs: Any) -> Any:
              **kwargs: Any kwargs.
         """
 
+    def setLevel(self, level: int) -> None:  # noqa: N802
+        """Set the log level
+
+        Args:
+            level: Log level to set as an integer
+
+        Returns:
+            None
+        """
+
 
 @runtime_checkable
 class DataclassProtocol(Protocol):


Test patch:
diff --git a/tests/app/test_app.py b/tests/app/test_app.py
--- a/tests/app/test_app.py
+++ b/tests/app/test_app.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+import logging
+
 import pytest
 
 from litestar import Litestar
@@ -11,3 +13,16 @@ def test_access_openapi_schema_raises_if_not_configured() -> None:
     app = Litestar(openapi_config=None)
     with pytest.raises(ImproperlyConfiguredException):
         app.openapi_schema
+
+
+def test_set_debug_updates_logging_level() -> None:
+    app = Litestar()
+
+    assert app.logger is not None
+    assert app.logger.level == logging.INFO  # type: ignore[attr-defined]
+
+    app.debug = True
+    assert app.logger.level == logging.DEBUG  # type: ignore[attr-defined]
+
+    app.debug = False
+    assert app.logger.level == logging.INFO  # type: ignore[attr-defined]