Repository: encode/starlette

Issue:
StaticFiles directory argument should take a PathLike instead of a str
It would be preferable to allow the `StaticFiles` app to use an `os.PathLike` so you can also provide `pathlib.Path` instances.  I can work on a PR if there's agreement.


Discussion:
Sure thing, yup!

Resolving patch:
diff --git a/starlette/responses.py b/starlette/responses.py
--- a/starlette/responses.py
+++ b/starlette/responses.py
@@ -4,9 +4,10 @@
 import json
 import os
 import stat
+import sys
 import typing
 from email.utils import formatdate
-from mimetypes import guess_type
+from mimetypes import guess_type as mimetypes_guess_type
 from urllib.parse import quote, quote_plus
 
 from starlette.background import BackgroundTask
@@ -30,6 +31,15 @@
     ujson = None  # type: ignore
 
 
+# Compatibility wrapper for `mimetypes.guess_type` to support `os.PathLike` on <py3.8
+def guess_type(
+    url: typing.Union[str, "os.PathLike[str]"], strict: bool = True
+) -> typing.Tuple[typing.Optional[str], typing.Optional[str]]:
+    if sys.version_info < (3, 8):  # pragma: no cover
+        url = os.fspath(url)
+    return mimetypes_guess_type(url, strict)
+
+
 class Response:
     media_type = None
     charset = "utf-8"
@@ -239,7 +249,7 @@ class FileResponse(Response):
 
     def __init__(
         self,
-        path: str,
+        path: typing.Union[str, "os.PathLike[str]"],
         status_code: int = 200,
         headers: dict = None,
         media_type: str = None,
diff --git a/starlette/staticfiles.py b/starlette/staticfiles.py
--- a/starlette/staticfiles.py
+++ b/starlette/staticfiles.py
@@ -15,6 +15,8 @@
 )
 from starlette.types import Receive, Scope, Send
 
+PathLike = typing.Union[str, "os.PathLike[str]"]
+
 
 class NotModifiedResponse(Response):
     NOT_MODIFIED_HEADERS = (
@@ -41,7 +43,7 @@ class StaticFiles:
     def __init__(
         self,
         *,
-        directory: str = None,
+        directory: PathLike = None,
         packages: typing.List[str] = None,
         html: bool = False,
         check_dir: bool = True,
@@ -55,8 +57,8 @@ def __init__(
             raise RuntimeError(f"Directory '{directory}' does not exist")
 
     def get_directories(
-        self, directory: str = None, packages: typing.List[str] = None
-    ) -> typing.List[str]:
+        self, directory: PathLike = None, packages: typing.List[str] = None
+    ) -> typing.List[PathLike]:
         """
         Given `directory` and `packages` arguments, return a list of all the
         directories that should be used for serving static files from.
@@ -71,11 +73,13 @@ def get_directories(
             assert (
                 spec.origin is not None
             ), f"Directory 'statics' in package {package!r} could not be found."
-            directory = os.path.normpath(os.path.join(spec.origin, "..", "statics"))
+            package_directory = os.path.normpath(
+                os.path.join(spec.origin, "..", "statics")
+            )
             assert os.path.isdir(
-                directory
+                package_directory
             ), f"Directory 'statics' in package {package!r} could not be found."
-            directories.append(directory)
+            directories.append(package_directory)
 
         return directories
 
@@ -154,7 +158,7 @@ async def lookup_path(
 
     def file_response(
         self,
-        full_path: str,
+        full_path: PathLike,
         stat_result: os.stat_result,
         scope: Scope,
         status_code: int = 200,


Test patch:
diff --git a/tests/test_staticfiles.py b/tests/test_staticfiles.py
--- a/tests/test_staticfiles.py
+++ b/tests/test_staticfiles.py
@@ -1,5 +1,6 @@
 import asyncio
 import os
+import pathlib
 import time
 
 import pytest
@@ -23,6 +24,19 @@ def test_staticfiles(tmpdir):
     assert response.text == "<file content>"
 
 
+def test_staticfiles_with_pathlib(tmpdir):
+    base_dir = pathlib.Path(tmpdir)
+    path = base_dir / "example.txt"
+    with open(path, "w") as file:
+        file.write("<file content>")
+
+    app = StaticFiles(directory=base_dir)
+    client = TestClient(app)
+    response = client.get("/example.txt")
+    assert response.status_code == 200
+    assert response.text == "<file content>"
+
+
 def test_staticfiles_head_with_middleware(tmpdir):
     """
     see https://github.com/encode/starlette/pull/935