Skip to content

Commit

Permalink
Use os.PathLike in StaticFiles for directory (#1007)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kevinastone committed Aug 5, 2020
1 parent 518da5e commit 9387832
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 10 deletions.
4 changes: 3 additions & 1 deletion docs/staticfiles.md
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
14 changes: 12 additions & 2 deletions starlette/responses.py
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 11 additions & 7 deletions starlette/staticfiles.py
Expand Up @@ -15,6 +15,8 @@
)
from starlette.types import Receive, Scope, Send

PathLike = typing.Union[str, "os.PathLike[str]"]


class NotModifiedResponse(Response):
NOT_MODIFIED_HEADERS = (
Expand All @@ -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,
Expand All @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions tests/test_staticfiles.py
@@ -1,5 +1,6 @@
import asyncio
import os
import pathlib
import time

import pytest
Expand All @@ -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
Expand Down

0 comments on commit 9387832

Please sign in to comment.