From 1c2bc252a606682cb570e828412d4b3e8703c8bc Mon Sep 17 00:00:00 2001 From: Kevin Stone Date: Mon, 20 Jul 2020 11:14:33 -0500 Subject: [PATCH 1/3] 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 --- starlette/responses.py | 2 +- starlette/staticfiles.py | 18 +++++++++++------- tests/test_staticfiles.py | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/starlette/responses.py b/starlette/responses.py index 1c9aaa114..527db9c5f 100644 --- a/starlette/responses.py +++ b/starlette/responses.py @@ -239,7 +239,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 From 8f5da0aededef6b6f37b3b91a9382f6a91425e83 Mon Sep 17 00:00:00 2001 From: Kevin Stone Date: Mon, 20 Jul 2020 11:41:10 -0500 Subject: [PATCH 2/3] Fixed `mimetypes.guess_type` not supporting PathLike on py3.7 and below --- starlette/responses.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/starlette/responses.py b/starlette/responses.py index 527db9c5f..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" From dad99c846e17128179af217671a3756b292ca476 Mon Sep 17 00:00:00 2001 From: Kevin Stone Date: Wed, 5 Aug 2020 10:17:27 -0500 Subject: [PATCH 3/3] Updated staticfiles documentation with `PathLike` param --- docs/staticfiles.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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