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/cli/_utils.py b/litestar/cli/_utils.py
--- a/litestar/cli/_utils.py
+++ b/litestar/cli/_utils.py
@@ -67,6 +67,7 @@ class LitestarEnv:
     host: str | None = None
     port: int | None = None
     reload: bool | None = None
+    reload_dirs: tuple[str, ...] | None = None
     web_concurrency: int | None = None
     is_app_factory: bool = False
 
@@ -97,6 +98,7 @@ def from_env(cls, app_path: str | None) -> LitestarEnv:
 
         port = getenv("LITESTAR_PORT")
         web_concurrency = getenv("WEB_CONCURRENCY")
+        reload_dirs = tuple(s.strip() for s in getenv("LITESTAR_RELOAD_DIRS", "").split(",") if s) or None
 
         return cls(
             app_path=loaded_app.app_path,
@@ -105,6 +107,7 @@ def from_env(cls, app_path: str | None) -> LitestarEnv:
             host=getenv("LITESTAR_HOST"),
             port=int(port) if port else None,
             reload=_bool_from_env("LITESTAR_RELOAD"),
+            reload_dirs=reload_dirs,
             web_concurrency=int(web_concurrency) if web_concurrency else None,
             is_app_factory=loaded_app.is_factory,
             cwd=cwd,
diff --git a/litestar/cli/commands/core.py b/litestar/cli/commands/core.py
--- a/litestar/cli/commands/core.py
+++ b/litestar/cli/commands/core.py
@@ -25,6 +25,9 @@ def _convert_uvicorn_args(args: dict[str, Any]) -> list[str]:
         if isinstance(value, bool):
             if value:
                 process_args.append(f"--{arg}")
+        elif isinstance(value, tuple):
+            for item in value:
+                process_args.append(f"--{arg}={item}")
         else:
             process_args.append(f"--{arg}={value}")
 
@@ -59,12 +62,14 @@ def info_command(app: Litestar) -> None:
 )
 @option("--host", help="Server under this host", default="127.0.0.1", show_default=True)
 @option("--debug", help="Run app in debug mode", is_flag=True)
+@option("--reload-dir", help="Directories to watch for file changes", multiple=True)
 def run_command(
     reload: bool,
     port: int,
     web_concurrency: int,
     host: str,
     debug: bool,
+    reload_dir: tuple[str, ...],
     env: LitestarEnv,
     app: Litestar,
 ) -> None:
@@ -87,14 +92,20 @@ def run_command(
     # invoke uvicorn in a subprocess to be able to use the --reload flag. see
     # https://github.com/litestar-org/litestar/issues/1191 and https://github.com/encode/uvicorn/issues/1045
 
+    reload_dirs = env.reload_dirs or reload_dir
+
     process_args = {
-        "reload": env.reload or reload,
+        "reload": env.reload or reload or bool(reload_dirs),
         "host": env.host or host,
         "port": env.port or port,
         "workers": env.web_concurrency or web_concurrency,
         "factory": env.is_app_factory,
     }
-    subprocess.run(["uvicorn", env.app_path, *_convert_uvicorn_args(process_args)], check=True)  # noqa: S603 S607
+
+    if reload_dirs:
+        process_args["reload-dir"] = reload_dirs
+
+    subprocess.run(["uvicorn", env.app_path, *_convert_uvicorn_args(process_args)], check=True)  # noqa: S603 S607I
 
 
 @command(name="routes")


Test patch:
diff --git a/tests/cli/test_core_commands.py b/tests/cli/test_core_commands.py
--- a/tests/cli/test_core_commands.py
+++ b/tests/cli/test_core_commands.py
@@ -32,6 +32,7 @@ def mock_subprocess_run(mocker: MockerFixture) -> MagicMock:
 @pytest.mark.parametrize("reload", [True, False, None])
 @pytest.mark.parametrize("web_concurrency", [2, None])
 @pytest.mark.parametrize("app_dir", ["custom_subfolder", None])
+@pytest.mark.parametrize("reload_dir", [[".", "../somewhere_else"], None])
 def test_run_command(
     mocker: MockerFixture,
     runner: CliRunner,
@@ -41,6 +42,7 @@ def test_run_command(
     host: Optional[str],
     web_concurrency: Optional[int],
     app_dir: Optional[str],
+    reload_dir: Optional[List[str]],
     custom_app_file: Optional[Path],
     create_app_file: CreateAppFileFixture,
     set_in_env: bool,
@@ -86,6 +88,13 @@ def test_run_command(
             args.extend(["--web-concurrency", str(web_concurrency)])
     else:
         web_concurrency = 1
+
+    if reload_dir is not None:
+        if set_in_env:
+            monkeypatch.setenv("LITESTAR_RELOAD_DIRS", ",".join(reload_dir))
+        else:
+            args.extend([f"--reload-dir={s}" for s in reload_dir])
+
     path = create_app_file(custom_app_file or "app.py", subdir=app_dir)
 
     result = runner.invoke(cli_command, args)
@@ -94,10 +103,12 @@ def test_run_command(
     assert result.exit_code == 0
 
     expected_args = ["uvicorn", f"{path.stem}:app", f"--host={host}", f"--port={port}"]
-    if reload:
+    if reload or reload_dir:
         expected_args.append("--reload")
     if web_concurrency:
         expected_args.append(f"--workers={web_concurrency}")
+    if reload_dir:
+        expected_args.extend([f"--reload-dir={s}" for s in reload_dir])
     mock_subprocess_run.assert_called_once()
     assert sorted(mock_subprocess_run.call_args_list[0].args[0]) == sorted(expected_args)
     mock_show_app_info.assert_called_once()