From 93878323e57e0bab92b4622849c67f5a7c96b24e Mon Sep 17 00:00:00 2001 From: Kevin Stone Date: Wed, 5 Aug 2020 18:04:07 -0500 Subject: [PATCH] Use os.PathLike in StaticFiles for directory (#1007) * Use os.PathLike in StaticFiles for directory This allows using `pathlib.Path` in addition to `str` for configuring the base directory of the static files in line with how python3.6+ handles filesystem operations. Fixes #1004 * Fixed `mimetypes.guess_type` not supporting PathLike on py3.7 and below * Updated staticfiles documentation with `PathLike` param --- docs/staticfiles.md | 4 +++- starlette/responses.py | 14 ++++++++++++-- starlette/staticfiles.py | 18 +++++++++++------- tests/test_staticfiles.py | 14 ++++++++++++++ 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/docs/staticfiles.md b/docs/staticfiles.md index aabc67d90..d8786af4d 100644 --- a/docs/staticfiles.md +++ b/docs/staticfiles.md @@ -5,7 +5,7 @@ Starlette also includes a `StaticFiles` class for serving files in a given direc Signature: `StaticFiles(directory=None, packages=None, check_dir=True)` -* `directory` - A string denoting a directory path. +* `directory` - A string or [os.Pathlike][pathlike] denoting a directory path. * `packages` - A list of strings of python packages. * `html` - Run in HTML mode. Automatically loads `index.html` for directories if such file exist. * `check_dir` - Ensure that the directory exists upon instantiation. Defaults to `True`. @@ -51,3 +51,5 @@ app = Starlette(routes=routes) You may prefer to include static files directly inside the "static" directory rather than using Python packaging to include static files, but it can be useful for bundling up reusable components. + +[pathlike]: https://docs.python.org/3/library/os.html#os.PathLike diff --git a/starlette/responses.py b/starlette/responses.py index 1c9aaa114..24dfb2c84 100644 --- 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 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 index 22b9d3ae6..41df98062 100644 --- 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, diff --git a/tests/test_staticfiles.py b/tests/test_staticfiles.py index 9e8101bc8..e2cae08f1 100644 --- 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 == "" +def test_staticfiles_with_pathlib(tmpdir): + base_dir = pathlib.Path(tmpdir) + path = base_dir / "example.txt" + with open(path, "w") as file: + file.write("") + + app = StaticFiles(directory=base_dir) + client = TestClient(app) + response = client.get("/example.txt") + assert response.status_code == 200 + assert response.text == "" + + def test_staticfiles_head_with_middleware(tmpdir): """ see https://github.com/encode/starlette/pull/935